CA2156880C - 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
CA2156880C
CA2156880C CA002156880A CA2156880A CA2156880C CA 2156880 C CA2156880 C CA 2156880C CA 002156880 A CA002156880 A CA 002156880A CA 2156880 A CA2156880 A CA 2156880A CA 2156880 C CA2156880 C CA 2156880C
Authority
CA
Canada
Prior art keywords
blr
boiler
fluid
phosphate
idc
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
CA002156880A
Other languages
French (fr)
Other versions
CA2156880A1 (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
Application filed by BetzDearborn Inc filed Critical BetzDearborn Inc
Priority to CA002415685A priority Critical patent/CA2415685C/en
Publication of CA2156880A1 publication Critical patent/CA2156880A1/en
Application granted granted Critical
Publication of CA2156880C publication Critical patent/CA2156880C/en
Anticipated expiration legal-status Critical
Expired - Lifetime legal-status Critical Current

Links

Classifications

    • CCHEMISTRY; METALLURGY
    • C02TREATMENT OF WATER, WASTE WATER, SEWAGE, OR SLUDGE
    • C02FTREATMENT OF WATER, WASTE WATER, SEWAGE, OR SLUDGE
    • C02F5/00Softening water; Preventing scale; Adding scale preventatives or scale removers to water, e.g. adding sequestering agents
    • C02F5/02Softening water by precipitation of the hardness
    • C02F5/025Hot-water softening devices
    • GPHYSICS
    • G01MEASURING; TESTING
    • G01NINVESTIGATING OR ANALYSING MATERIALS BY DETERMINING THEIR CHEMICAL OR PHYSICAL PROPERTIES
    • G01N33/00Investigating or analysing materials by specific methods not covered by groups G01N1/00 - G01N31/00
    • G01N33/18Water
    • GPHYSICS
    • G05CONTROLLING; REGULATING
    • G05DSYSTEMS FOR CONTROLLING OR REGULATING NON-ELECTRIC VARIABLES
    • G05D21/00Control of chemical or physico-chemical variables, e.g. pH value
    • G05D21/02Control of chemical or physico-chemical variables, e.g. pH value characterised by the use of electric means

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

~mssso APPARATUS AND METHOD FOR AUTOMATICALLY ACHIEVING AND
MAINTAINING CONGRUENT CONTROL IN AN INDUSTRIAL BOILER
SPECIFICATION
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 achieving and maintaining optimum congruency and phosphate concentration required to minimize corrosion in industrial high pressure boilers.
BACKGROUND OF INVENTION
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 chemicals which enter the boiler inadvertently (e. g., acid leaks) or intentionally (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." The term "cycles of concentration" is defined as the sum of the steam and blowdown 21~6~~'~
- flowrates divided by the blowdown flowrate. Cycles in high pressure boilers range from less than 10 to 100 or more. Thus a chemical added at a low concentration (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 the 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. Panson, 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 2~~~~~o maintained at a Na/PO' ratio that is below 3.0:1 , the boiler is said to be operating with coordinated phosphate/pH control (also known as "captive alkalinity" ) . 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 control 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 3000 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 that 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 2ns~~o 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 blowdown 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 operating boiler. In one general tubing 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 for 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 change 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 sodium/phosphate control in the literature which demonstrates the difficulties of this method and its shortcomings. In "A Practical Approach to Real Time Data 21~6~~~
- Acquisition and Automated Chemical Feed at a Fossil Fueled Cycling Duty Station", by C.E. Frederick presented at the International Conference on Cvcle 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-calibrations 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 task, 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,743 (Niemi), 4,239,493 (Niemi et al.), 4,181,951 (Boeke), 5,132,916 (Gulaian), 5,262,963 (Stance), 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 industrial boiler does not exhibit.
Consequently, the Niemi patent teaches controlling pH by use of a PID controller, which, as discussed previously, would be difficult 21~6~~~
to use in Corrosion Control Phosphate (CCP) programs described above.
The Niemi et al. patent discloses an automatic system for controlling the pH in a continuous flow 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 serious problem in a large volume system.
The Boeke patent discloses an automatic control system for the adjustment of pH that is described using the term "on-off".
However, this is not an ON/OFF 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 ~mss~~
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 pools. The apparatus controls two variables, i.e., chlorine and pH, under the assumption that the two are not interrelated. Although chlorine affects pH, chlorine has a minor effect on pH and can be isolated and controlled separately.
This is because in a swimming pool, chlorine is not the only buffering agent. Its contribution to the pH is masked by the high s 215fi~8~
'~- 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 does 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, and 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 from that of a boiler. The chlorine in the Schulenberg patent is not the major buffer, and no attempt is made 21~6~~~' "'- 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 linearized and performed incrementally to make the calculations simpler. The model used in the method/apparatus is not a true continuously stirred tank reactor (as is the model for industrial boilers). .
U.S. Patent No. 5,141,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 conducting this feed.
Two other references which discuss coordinated phosphate control are: Justification and Enqineerincr Design for the On-Line Monitorinq and Automation of a Congruent Phosphate/pH Procrram, by Michael E. Rogers, Ian Verhappen and Stephen Porter, Paper No. 413, The NACE Annual Conference and Corrosion Show 1992; Expert System Helps Fine-Tune Boiler-Water Chemistry, by Leyon O. Bretsel and Lon C. Brouse, Power Magazine 1987. In the former reference, although , --- a proposal is discussed for co ttol ing 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 congruency.
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 for on-line phosphate analyzers.
OBJECTS OF THE INVENTION
Accordingly, it is the general object 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/phosphate control that requires no tuning procedure.
It is still another object of the preferred embodiment of this invention to provide an automatic system for sodium/phosphate ~15~~8~
- 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 is yet another object 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 well 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 of this invention to provide an efficient method to achieve the congruency control (target) 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 phosphate 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 object 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 of 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 automatic 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.
SUMMARY OF THE INVENTION
These and other objects of the instant invention are achieved by providing an automatic control system for controlling at least two interdependent chemicals in the fluid of a continuously stirred ~1~~~~a ~- 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 interdependent chemicals in the fluid without controlling the effluent flow.
In addition, a second embodiment is provided for controlling the sodium-to-phosphate ratio and the phosphate concentration of a boiler fluid in an industrial boiler having a blowdown flow. The system comprises input means for receipt of a boiler fluid parameter and a parameter indicative of the phosphate concentra-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 with the accompanying drawings wherein:
Fig. 1 is a block diagram of the Model Adaptive Corrosion Control (MACC) system;
Fig. 2 is a block diagram of the input/output of the MACC
controller;

T~- Fig. 3 is a diagram showing the boiler water influents/effluents used by the 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. 6 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 periods 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. 10B 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 block 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. 1, 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 boiler to predict the feed rates of particular mixtures of 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 checks itself against its prediction and adapts its model to improve its control. The check is provided for by a laboratory pH and P04 analysis, rather than with the use of any on-line pH analyzers or P04 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 setpoint, as will be discussed in detail later.
The MACC system 20 is arranged to control water treatment chemicals to be introduced into an industrial boiler 22 by way of 21~~~~~
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 the 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 26 varies independently of the MACC system 20.
The MACC system 20 basically comprises a first chemical feedstream 30A, a second feedstream 30B, a control means 32 and an input means 34. The first chemical feedstream 30A and the second chemical feedstream 30B 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 that the sodium-to-phosphate ratio in the first chemical feedstream 30A 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 fluid 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 215~~~~
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 368 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 is arranged to precisely control the two chemical feedstreams 30A and 30B by controlling the operation of the pumps 34A and 34H in order to achieve and maintain a predetermined' desired sodium-to-phosphate 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 monitor/keyboard 46. The MACC system software 48 for effecting the operation of the control means 32 is set forth in Appendix A, along with an interface portion 45; the interface portion 45 interfaces the SMARTSCAN Plus software/firmware 44 with the MACC system software 48. The control means 32 also includes a feed pump controller 50 and the associated draw down assemblies 38A
and 38B.

The feed pump controller 50 and the associated draw down assemblies 38A and 38B 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 feed pump controller 50 receives the optimum feed rates for both pumps 34A and 348 from the MACC 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 52 takes the external inputs of time, pH, P04 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 (f8) of chemical pump 34A and the feed rate (fb) of chemical pump 348 to the feed pump controller 50 for precisely controlling pumps 34A and 34B at their respective feed rates. These controller 52-determined-feed rates are also fed back as 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 that 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 to back-calculate the blowdown rate, B, required to account for the change in boiler phosphate concentration observed. This eliminates the need for a direct blowdown flow measurement apparatus which is typically a costly device. Similarly, the last two pH samples, elapsed time between samples, feed rates and the estimated blowdown rate (B) are used, e~
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 model equations are run forward. In principle, all possible feed programs can be plugged into the model and the future boiler sodium/phosphate ratio and phosphate concentration predicted by the model compared with their control ranges and setpoints. As 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 Na/P04 ratio and phosphate concentration into their control limits (i.e., the target region, as will be discussed later), and once within the control limits, to the setpoint.
If a situation arose such that the chemical feed pumps 34A and 34B did not have the required pumping capacity to counteract an unusually large FCI, it would be impossible to bring the system 21~~~8~
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, f8 and fb, so that the distance between the model predicted steady-state boiler Na and P04 concentrations and the target region 60 (or setpoint 58) is minimized.
The model equations are based on the fact that the MACC
controller 52 assumes that only sodium and phosphate are present 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 PO': [HZP04'] monobasic phosphate, [HP04-] 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 - P04*B (Equation #1) where, ~P04=boiler (hence blowdown) phosphate concentration, moles/kg;
~fa feed rate (kg/hr) for chemical pump 34A:

21~~~~~
-- ~fe feed rate (kg/hr) for chemical pump 34B:
~P04a=feed concentration (moles/kg) for phosphate from tank 36A:
~P04ti 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, f8, P048, fb, P04b and H remain fixed, yields the following chemical concentration functions:
P04(t) - ( (fa*P048 + fb*P04b)/B) * (1-e~'LiT~) + P04 (0) *e~-tit~
(Equation #2) Na (t) - ( ( fa*Nae + fb*Nab +L) /B) * ( 1-e~''~'~) + Na (0) *e~'t~''~
(Equation #3) where ~r = first order time constant = M/B;
~Nae feed concentration (moles/kg) for sodium from tank 36A:
~Nati 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 concentration.
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 addition of the FCI, L, (also assumed constant) into the boiler 22.
It is apparent that unlike phosphate, which is directly measured by the operator, the sodium concentration is not directly measured. Therefore, the sodium concentration must be computed indirectly using measured pH corrected to 25°C and measured P04.
Using the previous assumption that only sodium and phosphate are active in determining congruency, the sodium concentration, Na, can be computed, assuming electroneutrality in the boiler water, 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, fey, fb~ for a length of time, dt~ so as to bring the Na/PO' ratio and phosphate within their control limits. Similarly, the pumps 34A and 34B are held at fat, f~ for time interval, dtZ, in order to bring the Na/P04 ratio and phosphate to their setpoints. Finally, once these setpoints are reached, the steady state feed rates, f83, 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 (dtt, dt2, fey, fb~, f~, f~, f~, 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 to the setpoint as soon as possible (minimizing dtz).
Chemically correct congruent control is obtained by choosing a narrow control range around the setpoint for Na/P04 ratio, and choosing as wide a control range as acceptable for phosphate. This allows the algorithm to focus on getting the critical Na/P04 21~6~~~
ratio correct initially, except when phosphate levels are so far off that they present an equally critical control problem. For this reason, by default, the 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 operational pH variability. In particular, there is shown in Fig. 5, the desired congruency 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 fey, fb~, etc.) to back-calculate the blowdown flow 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=( (fag*P04a+fb~*P04b)/B) * (1-e~'dcn,~)+lastP04*e~'dt~iT~
(Equation 2A) P042=( ( fee*P04a+f~*P04b) /B) * ( 1-e~'dtzi'~ ) +p041*e~'dt2i'~
(Equation 2B) 21558~fl P043=( (f~*P04a+f~*P04b)/B) * (1-e~'dt3/r))~.p042*e~'dt3/r) (Equation 2C) wherein the concentration at the end of stage 1 is substituted for the initial concentration at the beginning of stage 2, etc.
If the estimated blowdown flow rate is exactly right, then P043 (the predicted concentration at 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 blowdown estimate, the smaller the predicted phosphate 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 electroneutrality of the boiler water, the current and preceding pH and P04 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 ingress, L, is determined using Equation #3 sequentially and by comparing the Na predicted 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 no reasonable 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 phosphate at the end of stage 3 would be at its maximum. A P04 entry greater than this maximum would be 21~6~8~
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) in the MACC 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, lastP04, pH, and PQ4 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 variability in the 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_INDETERMINATE flag is raised, and MACC
controller 52 continues to use the 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~, dtz, dt3, fag, fb~, fee, f~, f83, and f~), blowdown (B) and feedwater contaminant ingress (L), and the initial boiler phosphate concentration and pH, the Equations #2, #3 and the charge balance equation, can 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 not 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 P04 (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 (f8), chemical pump 34B feed rate (fb), FCI (L), and blowdown flow rate (B), the successive "states" (P04, 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 concentrations. Furthermore, according to Equations #2 and #3, the boiler concentrations must attain their steady state levels (t=~), namely:

21~~~~0 - P04(t~) - (fa*P04a + fb*P04b)/B (Equation #4) Na(t~) - (fa*Naa + fb*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 it.
Hence, the curve is defined by a straight line between the initial point (P04(0), Na(0)) and 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 time t is the same for both PO~ and Na; changes in "y" (Na) are always in the same proportion to changes in "x" (P04) -- 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 equilibrium 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 equilibrium point the velocity approaches zero (which corresponds with the exponential functions in the time domain, i.e., the derivative of 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 215~~~~
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 #7 These simple, straight line, state space trajectories provide a method of considering the entire range of boiler concentrations that are attainable from a given starting point using a particular set of feed rates.
The possible "end points" of these straight lines (Equations #4 and #5) are defined by the range of the chemical feed pumps, 34A
and 34B:
f~~~ <= fa <= fix where f~~~ is usually 0:
f~~~ <= fb <= f~X where f~~~ 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, f8 and fb) in the state space hereinafter known as the pumpable~region 62:
P04 (~) (P04a/B) (P04e/B) (0) f, + fb + (Equation #8) Na(~) (Naa/B) (Nae/B) (L/B) This pumpable region 62 is shown in Figure 6. Note that each point in the pumpable region 62 uniquely defines the feed rates (f8, fb) associated with the corresponding steady state boiler concentrations, provided only that the concentrations in chemical 2156~8~
pumps 34A and 34B are linearly independent (in other words, P04a*Nab - P04b*Nas must not be equal to 0). This is an important requirement of the MACC controller 52.
By holding 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 to 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 controller 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 the current point 54 and end in the pumpable region 62. In general, some of these segments (Fig. 7) will cross into the target region 60, and some will not. In the special case in which 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 pumpable 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 70 on the pumpable 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 traj ectory 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. Note 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 combination 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 used (Fig. 8).
The next determination that the MACC 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 21~fi~~0 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 lie on a line 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 trajectories that passes through the endpoints of the target edge (Fig. 10). As can be seen in Fig. l0, there are eight line segments, each having two collinear trajectories that must be evaluated. The MACC controller 52 exploits this fact by checking each of the, no more than 16, such trajectory scenarios, and then using 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 candidate trajectories can be more easily seen in Figs. 10A and lOB

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 1 adjustment, stage 2 is easier. As shown in Fig. 11, first, the current point 54 is replaced with the point 68 on the target 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 82: the point on the pumpable region's perimeter found by extending this line segment beyond the setpoint 58 must, by the argument given earlier, correspond to the feed rates ( fez, f~) that reach the setpoint 58 in the least time. If there is no such point, the MACC controller 52 sets the E POINTUNREACHABLE flag and uses the feed rates that correspond to the pumpable point within the target region 60 that is closest to the setpoint.
For stage 3, the MACC controller 52 simply feeds at the feed rates that correspond to the setpoint (f~, 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, as discussed below.
Blowdown and FCI Calculation: The blowdown (B) and FCI (L) estimates are exact: they are the roots of equations that have at most one solution. This could introduce problems 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, throw away the intermediate data points until enough time has elapsed so that a meaningful feed program determination can be made. Therefore, in 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 would give the MACC system 20 the desirable property that more frequent operator sampling would always result in better control.
Introduction of Dead Time: Another way that the MACC system 20 could be improved is by explicitly 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 degrade control much, provided that dead time is a small percentage of the boiler residence time. However, the impact of ~1~68~0 °- dead time depends on many factors; it is likely 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 Program Determination: the linear programming-related-optimization of the MACC controller 52 feed program determination method could be generalized in a number of ways.
Firstly, the setpoint 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 limit the application of this feed program optimization to just two dimensional state spaces. For example, assuming that polymer measurements were possible, one could introduce a three dimensional state space/target region; if ~1~~~~~
-- such a system were still limited to two pumps, this would result in a planar pumpable region being embedded in a 3-D space.
Ammonia Correction: Boilers occasionally have ammonia returned to the boiler. Once in the 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 provides a method for correcting the measured pH for the effect of ammonia: an analytical determination of the ammonia in the boiler water is provided by any conventional ammonia measuring means (e. g., colorimetric, ion-selective electrode, ion chromatography) to the MACC 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. Use of the ammonia correcting method can be implemented as an option in the MACC
system 20 that the operator can select so that when the pH and phosphate analysis data is 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 meters/phosphate analyzers) for boiler water parameters rather than using manual data entry of these parameters. Although one of the novel aspects of the MACC system 20 is that it eliminates the need to have on-line instrumentation which is susceptible to breakdown and requires frequent calibration, the MACC system 20 is 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, all 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 is made, the MACC system 20 estimates the blowdown flow and FCI.
Using the blowdown flow measurements made over the interval between samples, the MACC controller 52 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 mass imbalance represents a measurement of any ~1~6~~~
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 arid phosphate imbalance estimates, Equations #2A, #2B, and #2C would be expanded to included additional stages so as to incorporate changes in the measured blowdown flow into the model predicted 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 P04 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 phosphate 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 OLBF measurements, would be used to detenaine these projections. The resulting calculated feedrates would then be transmitted to the pumps by the control means 32.

As also discussed earlier, the MACC system 20 uses three stages to reach the setpoint 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, because the updates would be so frequent, the system 20 would rarely get beyond the current stage before another update arrived.
With OLBF/pH or OLBF/P04, the same steps discussed with OLBF
would be carried out. 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, to obtain a model-projected phosphate. Using the charge balance equation as before, this model-projected phosphate and 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 concentrations and/or estimated boiler sodium concentrations are used, along with the differential mass balance equations shown below, to estimate P04 and/or FCIs.
M*(d(Na(t))/dt = L_Na(t) + fa(t)*Nae + fb(t)*Nab -B(t)*Na(t) Equation #9 . .
"~- M* (d(P04 (t) )/dt = L P04 (t)+f$(t) *P048 + fb(t) *P04b -B(t) *P04 (t) Equation #10 The above equations are solved for L_Na(t) and L_P04(t), and these instantaneous L_Na(t) and L_P04(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 (fe(t), fb(t)) needs to be shifted to incorporate deadtime explicitly; otherwise, it is necessary to use averaged/filtered L Na(t) and L P04(t) that are representative of time intervals large enough so that the impact of deadtime on these averaged /filtered estimates is negligible.
These averaged/filtered L-Na(t) and L P04(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 20 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 FCI 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 present with any automated control scheme.

~~s~~~
Multiple Boiler Control: The MACC system 20 is readily adaptable to controlling multiple (e.g., ten) boilers 22 at a customer's facility. To 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 least desirable state will receive the MACC
system 20 congruent control initially.
Note: the definition of the word "automatic" 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 negate the automatic operation based on a manually-entered input (e. g., measured phosphate concentration).
In addition, operator response to automatically-generated error messages, alarms, etc. do not 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 220. As with the MACC system 20, the ON/OFF
control system 220 controls two interdependent variables, namely, phosphate and sodium, the latter 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 is also referred to as the "effective sodium" or the "effective sodium-to-phosphate ratio."
The ON/OFF control system 220 is arranged to control water treatment chemicals to be introduced into an industrial boiler 222 by way of the feedwater 224 to the boiler 222. The boiler 222 has an effluent flow (hereinafter known 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 of the ON/OFF control system 220 over conventional boiler fluid control systems is that the blowdown flow 226 varies independently of the ON/OFF control system 220.
The system 220 basically comprises a first chemical feedstream 230A, a second chemical feedstream 230B, a control means 232 and an input means 240. The first chemical feedstream 230A and the second chemical feedstream 230B 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 particular sodium-to-phosphate ratio and a known phosphate concentration, to the water in the boiler 222.
Similarly, the second chemical feedstream 230B is arranged to deliver a second fluid treatment material or chemical, e.g., a sodium phosphate mixture having a particular sodium-to-phosphate ratio and a known phosphate concentration to the water in the boiler 222. It is important to note 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 230B while the respective known phosphate concentrations in the feedstreams can, in certain circumstances, be identical. The particular sodium-to-phosphate ratio (Na/PO4) determines a particular pH for that fluid treatment material.
Where the known phosphate concentration (P04) in both feedstreams is identical, then the different sodium concentrations (Na) in each feedstream determine the pH (high 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 the boiler water. Thus, monitoring the pH
of the boiler water provides an effective method of monitoring the sodium-to-phosphate ratio 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 permits the sodium-to-phosphate ratio of the boiler water to be controlled.
Each feedstream 230A and 230B includes an electrically driven pump 234A and 234B which are coupled to the outlet of respective chemical feed tanks 236A and 236B, via respective draw down assemblies 238A and 2388. 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 2348 by the control means 232.
The control means 232 is arranged to precisely control the two chemical feedstreams 230A and 230B by controlling the operation of the pumps 234A and 2348 in order to achieve and .maintain a predetermined desired sodium-to-phosphate ratio in the boiler water while maintaining a predetermined fixed phosphate concentration (P04 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 feedstream 230A or 230B
pumps at a rate proportional to the blowdown flow 226 in order to maintain the fixed phosphate concentration.
The control means 232 basically comprises a computer-based control unit 242 such as that sold by Betz Laboratories, Inc. under the mark SMARTSCAN Plus and associated software/firmware 244 and a monitor/keyboard 246. The ON/OFF control system software 248 and SMARTSCAN Plus software 244 for effecting the operation of the control means 232 is set forth in Appendix B. The control means 232 also includes a feed pump controller 250 and the associated draw down assemblies 238A and 2388.

21~6~~~
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 234B 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 the 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 is delivering its respective chemical mixture to the boiler 222, the chemical mixture feed is in proportion to the blowdown flow 226. In particular, the ON/OFF
control system software 248 implements this proportion by the following equations:

Lo Na_flow = P04 setpoint * (BD flow)/(Lo P04_conc)(Equation #11) Hi Na-flow = P04-setpoint * (BD_flow)/(Hi_P04_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 concentration in tank 236B;
The pH meter 254 monitors the pH of the blowdown flow 226 and provides a corresponding electrical 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/OFF 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 250, the two feedstreams 230A and 2308 and the blowdown flowmeter 252. Since the phosphate concentration in each chemical feed tank 236A and 236B is known and the predetermined fixed phosphate concentration (P04 setpoint) has been entered into the computer 242 by the operator, the feed pump controller 250 uses the signal 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 236B, i.e., an even distribution of tank amount is used to avoid emptying one tank faster than the other.
The second area of control is through the switching between high-pH and low-pH sodium phosphate mixtures of the first chemical feedstream 230A and the second chemical feedstream 230B. This action is conducted by the control means 232. In particular, the ON/OFF control system software 246 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 234B
to feed the low-pH sodium phosphate mixture in the second feedstream 2308 at the proportioned rate (Equation #11) while turning off the other pump 234A in the first chemical feedstream 230A, thereby driving the boiler 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 #12) while turning off the other pump 2348 in the second chemical feedstream 2308, thereby driving the boiler water pH value up to the pH setpoint. In either situation, whichever feedstream is delivering its particular sodium phosphate mixture, the feed pump controller 250 maintains the fixed phosphate concentration in the boiler water by ratioing the active feedstream 230A or 2308 to the blowdown 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 particular, the ON/OFF control system software 248 implements this 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 conc) where, pH: current pH value of boiler water or exponential weighted moving average of n past pH values;
pH setpoint: pH setpoint Should the first chemical feedstream 230A and the second feedstream 230B 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) ~156~8~1 '' 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 possible that the expected and actual phosphate concentrations can start to diverge due to calibration problems, pump leaks, or actual leaks in the boiler. In that case, some means of monitoring the concentration and informing the operator that action to correct the boiler phosphate mass imbalance is desirable. One exemplary method of 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 ON/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. As an alternative to using the phosphate analyzer 256, the blowdown phosphate concentration could be manually measured and the measured phosphate concentration can be entered into the computer 242 at various time intervals.

21~6~~Q
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.

y , /* ,~nacc4 . h: external interface for the macc4 . c module */ C A 215 6 8 ~ p /* My ?9, 1994: added nh3, lastnh3, and b4lastnh3 to support ammonia coy ection to the pH -- JCG */
# include <float.h> /* uses only FLT MAX */
# define E_ENGINEERING UNITS 1 /* 0=resea~ch units, 1=engineering units*/
# define E NAN (-l.Oe37) /* 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 MAXBISECTIONS 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-logical { E FALSE=0, E TRUE=1 } E LOGICAL;
typedef struct a vector { double po4; double na; } EVECTOR;
typedef enum e_initstatus { /* e_init() errors/alarms */
E INITOK=0, E MISSING T, E MISSING FAMIN, E MISSING FAMAX, E MISSING FADEF, E MISSING P04A, E MISSING RATIOA, E MISSING FBMIN, E MISSING FBMAX, E MISSING FBDEF, E MISSING P04B, E MISSING RATIOB, E MISSING P04, E MISSING PH, E MISSING M, E MISSING BDTEMP, E MISSING P04SETPOINT, E MISSING MINP04, E MISSING--MAXP04, E MISSING RATIOSETPOINT, E BDMAX LT BDMIN, E NALEAFQ~IAX LT NALEAKMIN, E_FAMAX LE FAMIN, E FBMAX LE FBMIN, E MINP04_LT MINMINP04, E MAXP04 GT MA~CMAXP04 , E P04SETPOINT LT MINP04, E P04SETPOINT GT_MAXP04, E P04A_LT ZERO, E P04B LT ZERO, E MINRATIO LT MINMINRATIO, E MAXRAT I O GT MA~CMA~~RAT I O , E RATIO LT MINRATIO, E RATIO GT MAXRATIO, E RATIOA EQ RATIOB, E MISSING SPGA, E MISSING SPGB, E INITSTATUS;

t~~~edef enum a updatestatus { /* a update() errors/alarms */
E UPDATEOK=0, E _ '7ETERMINATE=1, E 't~X UNREACHABLE=2 , E OUTOFBOX TOOLONG=4, E POINT UNREACHABLE=8, E BOX UNMAINTAINABLE=16, E POINT UNMAINTAINABLE=32, E SAMPLEINTERVAL TOOLONG=64, E NOTUPDATED=128, E INCONSISTENT P04=256, E INCONSISTENT PH=512 E UPDATESTATUS;
typedef struct /*
e_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 */

s pga; /* specific gravity */

double f bmin, /* pump b =>
mininum feed rate */

fbmax, /* maximum feed rate fbdef, /* default (initial) feed rate */

po4b, /* total (as rtho-P04) concentration*/

ratiob, /* ~
Na/P04 ratio spgb; ~ gravity */
s f double m; * *j 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; /* boiler blowdown ammonia concentration */

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

d ph, /* variability */

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

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

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

double napo4ratio, /*
alternative to phsetpoint */

minnapo4rati o, /*
alternative to phsetpoint-dphsetpoint */

maxnapo4rati o; /*
alternative to phsetpoint+dphsetpoint */

double max-sample-i nterval;
/*
if time between samples is longer than*/

/*
this, an updatestatus code is flagged */

double max_outofbox _adjustment;
/*
if we cannot get into control ranges on */

% *
pH
and within this time, an */

/*
updatestatus code is flagged */

double max error;
relative /*
largest allowed variation in estimated */

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

/* dP04 before an E
INDETERMINATE
status is flagged */
-double beps, /*
bisection (root finding) epsilon */

bdmin, /*
lower bound for blowdown bisection */

bdmax, /*
upper bound for blowdown bisection*/

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

naleakmax; /*
upper bound for Na leak bisection */

/* (final rootfinding due to truncation errors will be */
errors on /* beps*(~bdmax~ ~bdmin~ ) beps*(~naleakmin~+~naleakmax~) */
+ and double fdt; to /* time feed (due del to hardware truncation):
*/

/* ~ inimum m time interval between pump changes */

/ * ~orivate : * /
double lastt, lastbdtemp, lastpo4, lastnh3, lastph; /*last inputs... */
dot ' b4lastt,b4lastbdtemp,b4lastpo4,b41astnh3, b4lastph;/* & before last */
doubze bd, 1; /* blowdown, leak estimates */
double lastbd, lastl; /* prev blowdown, leak estimates */
double dt [2] , fb [3] , fa [3] ; /* current feed program */
double lastdt[2], lastfb[3], lastfa[3]; /* previous feed program */
E VECTOR vV[E NV+1], vW[E NW+1]; /* pumpable, target regions */
E VECTOR vlastV[E NV+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 lastupdatestatus; /* previous updatestatus */
E INITSTATUS initstatus; /* identifier for a init() errors*/
E LOGICAL undoable; /* can last a update be undone ? */
E BOILER;
const E BOILER *e undefined boiler(void) E INITSTATUS a init(E BOILER *blr);
double a afeedTE BOILER *blr);
double a bfeed(E_BOILER *blr);
E UPDATESTATUS a update(E BOILER *blr);
E LOGICAL a undoTE BOILER *blr);
double a hmp2po4(double hmp);
double e_po4model(E BOILER *blr);
double e~hmodel (E BOILER *blr) ;
double a nasetpoint(E BOILER *blr);
double a congruencyratio(E BOILER *blr);
double e=fixNAN(double number, double forNAN);

CA215688~
/*tmacc4.c: module for model based coordinated phosphate/ph control */
/*
April 4, 1994 : added lines near the end of a 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 a 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 () cah) .
May 29, 1994 - added support for an ammonia correction to the pH as per Scot Boyette's request.
June 3, 1994 - added an a congruencyratio() exported function to support callers who want 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 blr->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.67) # define E GPD TO KGPERHR(gpd,spg) ((E LITERS_PER GALLON/24.0)*(spg)*(gpd)) # define E PPM TO MOLESPERKG(ppm,mw) (Tppm)*1 Oe-3/(mw)) # define E MOLESP~,RKG TO PPM(mpk,mw) ((mpk)*l.Oe+3*(mw)) # define E PERCENT TO MOLESPERKG(percent,mw) ((percent)*l.Oe+1/(mw)) # define E LB TO R~(lb) ((lb)*0.45359) # define E KG TO LB(kg) ((kg)/0.45359) # de f ine E F2IZEL~IN ( f ) ( ( 5 . 0 / 9 . 0 ) * ( ( f ) - E ZERO K IN F ) ) # define K DEFAULT TEMP 298.15 /*room temperature in degrees K */
# define K DEFAULT NH3 0.0 /*for Na:P04 ratio setpoints, in moles/kg */
# define K DEFAULT RHO 0.997047 /*room temperature density of saturated H20*/

#i,'_~E ENGINEERING UNITS /* this compilation switch is set in macc4.h */
/*C versions require internalunits:
to obtain MACC4's */

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

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

* P04 fwws cone: % 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 TO 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) # define FBDEF(blr) E GPD TO KGPERHR((blr)->fbdef,(blr)->spgb) # define P04B(blr) E PERCENT TO MOLESPERKG((blr)->po4b, E MW P04) # define NAB(blr) ((bIr)->ratiob*P04B(blr)) - -# define FDT(blr) ((blr)->fdt) # define USERP04(po4) E MOLESPERKG TO PPM(po4, E MW P04) # define P04(blr) E PPM TO MOLESPERKGT(blr)->po4, E MW P04) # define LASTP04(blr) E PPM TO MOLESPERKG((blr)->lastpo4, E MW_P04) # define DP04(blr) E_PPM TO MOLESPERKG((blr)->dpo4, E MW P04) # define MINP04(blr) E PPM TO MOLESPERKG((blr)->minpo4, E MW P04) # define MAXP04(blr) E PPM TO MOLESPERKG((blr)->maxpo4, E MW P04) # define P04SETPOINT(bIr) E PPM TO MOLESPERKG((blr)->po4setpoint, E MW P04) # define NH3 (blr) \ - - - - -E PPM TO MOLESPERKG(e fixNAN((blr)->nh3,K DEFAULT NH3), E MW NH3) # define LASTNH3(blr) \- - - - -E PPM TO MOLESPERKG(e fixNAN((blr)->lastnh3,K DEFAULT NH3), E MW NH3) # define LASTPH(blr) ((Flr)->lastph) - - - -# define PH(blr) ( (blr) ->ph) # define DPH(blr) ((blr)->dph) # define PHSETPOINT(blr) ((blr)->phsetpoint) # define DPHSETPOINT(blr) ((blr)->dphsetpoint) # define LASTT(blr) ((blr)->lastt) # define T(blr) ((blr)->t) # define M(blr) E LB_TO KG((blr)->m) # define BDTEMP(bIr) E F2KELVIN(blr->bdtemp) # define LASTBDTEMP(blr) E F2KELVIN(blr->lastbdtemp) # define MAX SAMPLE INTERVAL(blr) ((blr)->max sample interval).
# define MAX OUTOFBOX ADJUSTMENT(blr) ((blr)->max outofbox adjustment) # define BEPS(blr) ((blr)->beps) - -# define NALEAKMIN(blr) ((blr)->naleakmin) # define BDMIN(blr) E LB TO KG((blr)->bdmin) # define NALEAFQHAX(blr) ~(bIr) ->naleakmax) # define USERBD(bd) E KG TO LB(bd) # define BDMAX(blr) E LB TO KG((blr)->bdmax) # define MAX RELATIVE ERROR~blr) (((blr)->max relative error)/100.0) # define BD(blr) E LB TO KG( (blr) ->bd) - -# define L(blr) ((blrT->I) # define DT (blr, n) ( (blr) ->dt [n] ) # define FA(blr,n) E GPD TO KGPERHR( (blr) ->fa [n] , (blr) ->spga) # define FB(blr,n) E GPD TO_KGPERHR((blr)->fb[n], (blr)->spgb) # define DEFAULT DP04 0.5 # 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 20.0 /*20% error allowed by default*/
# define DEFAULT MAX SAMPLE INTERVAL (24.0*7.0) # define DEFAULT BEPS 1.0e-12 . ' #el~se /* research units */ C A 215 6 8 8 0 # define FAMIN(blr) ((blr)->famin) # de. ~e FAMAX (blr) ( (blr) ->famax) # define FADEF(blr) ((blr)->fadef) # define P04A(blr) ( (blr) ->po4a) # define NAA(blr) ((blr)->ratioa*P04A(blr)) # define FBMIN(blr) ((blr)->fbmin) # 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) # define USERP04(po4) (po4) # define P04(blr) ((blr)->po4) # define DP04(blr) ((blr)->dpo4) # define MINP04(blr) ((blr)->minpo4) # define MAXP04(blr) ((blr)->maxpo4) # define P04SETPOINT(blr) ((blr)->po4setpoint) # define LASTNH3(blr) (e fixNAN((blr)->lastnh3, K DEFAULT_NH3)) # define NH3(blr) (e fixNAN((blr)->nh3, K DEFAULT NH3)) # define LASTPH(blr)-((blr)->lastph) # define PH(blr) ((blr)->ph) # define DPH(blr) ((blr)->dph) # define PHSETPOINT(blr) ((blr)->phsetpoint) # define DPHSETPOINT(blr) ((blr)->dphsetpoint) # define LASTT(blr) ((blr)->lastt) # define T(blr) ((blr)->t) # define M (blr) ( (blr) ->m) # define BDTEMP(blr) (273.15 + (blr)->bdtemp) # define LASTBDTEMP(blr) (273.15+(blr)->lastbdtemp) # define BLRTEMP(blr) (273.15+(blr)->blrtemp) # define MAX SAMPLE INTERVAL(blr) ((blr)->max sample interval) # define MAX OUTOFB~X ADJUSTMENT(blr) ((blr)->max outofbox adjustment) # define BEPS(blr) ((blr)->beps) # define NALEAKMIN(blr) ((blr)->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) ((blr)->bd) # define L (blr) ( (blr) ->1) # define DT (blr, n) ( (blr) ->dt [n] ) # define FA (blr, n) ( (blr) ->fa [n] ) # def ine FB (blr, n) ( (blr) ->fb [n] ) # define DEFAULT DP04 E_PPM_TO_MOLESPERKG(O.S,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*/
# define DEFAULT MAX SAMPLE INTERVAL (24.0*7.0) # define DEFAULT BEPS 1.0e-12 #endif # define ROUNDED DT(blr,n) (e round down(DT(blr,n), FDT(blr))) ea2~ 5b8so /*~Misc. Functions */
doub a round down(double x, double dx) { /* round x down to multiple of dx */
reo,..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 */
returnZ(x > y) ? x . y);
double a forceinrange(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 ln(x) */
/* Basic Vector Operations */
#define VBUF 100 E VECTOR *e vec(doub7:e po4, double na) { /* convert 2 components to vec*/
static E ~IECTOR 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, expressions with more than */
v[ibuf].na = na; /* VBUF a vec() calls will have problems)*/
return(&v[ibuf]); /* Approach needed to work around an MS-C bug*/
/* when passing structures to/from functions*/
double a dot(E VECTOR *vl, E VECTOR *v2) { /* inner (dot) product of 2 vecs*/
returnZvl->po4*v2->po4 + vl->na*v2->na);
double a norm(E VECTOR *v) { /* magnitude (norm) of vector */
} returnTsqrt(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, ti{es a vector, v */
E VECTOR *e sXv(double s, E VECTOR *v) -return(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.0/s, v));
%* a vPv: add ("P") two vectors */
E ~{IECTOR *e vPv(E VECTOR *vl, E VECTOR *v2) }eturn(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, E VECTOR *v2) {
-return(e vPv(vl, a sXv(-1.0, v2)));

/*~e vEv: Are two vectors approximately equal, within machine precision ? */
E_LO''"~AL a vEv(E VECTOR *vl, E VECTOR *v2) ( re,,_~n ( ~e norm(e_vSv(vl,v2)r <= 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));
} _ /* a between: given three co-linear points, <C> ? */
is <B> between <A> and E_LOGICAL a between(E VECTOR *vA, E VECTOR
*vB, E VECTOR *vC) return( (e norm(e vSv(vA,vB)) <= e norm(e vSv(vA,vC)) &&

_ E FALSE);
_ _ e_norm(e-vSv(vB,vC)) <= a norm(e vSv(vA,vC))) ? E TRUE
_ _ _ .

/* a decompose: decomposes a given vector into the given ~~coordinate system's .
Specifically, computes alpha and beta such that:

<A> _ <Origin> + alpha*<XX> + beta*<YY>.

Here <A>, <Origin>, <XX>, and <YY> are vectors, scalars.
alpha and beta are Note: <XX> and <YY> must be linearly independent and non-zero, or divide by 0 errors will occur (macc4 prevents such a possibility by various up front c hecks).

*/

void a decompose(E_VECTOR *vA, E VECTOR *vOrigin, VECTOR
*vYY, E VECTOR *vXX, E
d bl *
l h *
- -ou e a p a, ouble beta) ( E VECTOR *vx, *vu, *vuperpx, *vYYperpx;

vac = a vDs ( vXX , a norm ( vXX ) ) ;

vu = e-vSv(vA,vOrigin);

vuperpx = a vSv(vu, a sXv(e dot(vu,vx), vx));
-vYYperpx = a vSv(vYY, a sXvTe dot(vYY,vx),vx));

*beta = a dot(vuperpx,vYYperpX)/e dot(vYYperpx,vYYperpx);

*alpha = e_dot(vx, e_vSv(vu, e_sXv(*beta,vYY)))/e norm(vXX);.
_ /* Functions for ph to na Conversion */
/* d'_ ~ho: density of water along the saturation line ("psat") */
/* (i-~.luded from CMS 2.0 module db2.c) */
/* 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 [0] ) ;
double rho = a [--i] ; /* compute "sum over i of a [i) *t**i" */
if (fabs(K DEFAULT TEMP-t) <= K DEFAULT TEMP*DBL EPSILON) /* to avoid "fit of a fit" errors */
return(K DEFAULT RHO); /* for special case of room temp values*/
~* use the exact value (as per S. Boyette)*/
do {
rho *= t;
rho += a [--i) ;
} while (i > 0);
rho /_ (1.0 + 0.003582*t);
return(rho);
}
/* a na: Back calculates an "equivalent na" in the boiler, using tfie charge balance equation and measured po4 and ph in the boiler.
po4, na concentrations are in moles/kg.~Temperature in Kelvin. Activity is assumed 1.0 for simplicity (no ionic strength correction).
See CMS (Condensate Modelling 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 K1 - 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 - how (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, - (12.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*(K1/H)*(K2/H) + 3.0*(K1/H)*(K2/H)*(K3/H))/
} (1.0 + (K1/H) + (K1/H)*(K2/H) + (K1/H)*(K2/H)*(K3/H)));
/*e congruencyratio: exported congruency Na:P04 ratio calculation */
double a congruencyratio(E_BOILER *blr) {
if (E NAN =- blr->po4 ~~

,~ E.NAN =P04bibl;ph~~) return(E NAN);
e-'return (e na (P04 (blr) , PH (blr) , e_fixNAN(BDTEMP(blr), K DEFAULT TEMP) a fixNAN(NH3(blr), K DEFAULT NH3) 7 P04(blr) double a napo4ratio(E BOILER *blr) { /* napo4ratio setpoint for boiler */
' return ((blr->napo4ratio != E NAN) ? (blr->napo4ratio) 'e na(P04SETPOINT(blr), PHSETPOINT(blr), K DEFAULT TEMP, K DEFAULT NH3)/
~. P04SETPOINT(blr)) ); - - - -double a minnapo4ratio(E BOILER *blr) { /*min napo4ratio setpoint for boil er /
return ((blr->minnapo4ratio != E_NAN) ? (blr->minnapo4ratio) e_max(E MINMINNAP04RATI0, e_naZP04SETPOINT(blr),PHSETPOINT(blr)-DPHSETPOINT(blr), K DEFAULT TEMP, K DEFAULT NH3) / P04SETPOINT(blr)) );-double a maxnapo4ratio(E BOILER *blr) { /*max napo4ratio setpoint for boiler */
return ((blr->maxnapo4ratio != E_NAN) ? (blr->maxnapo4ratio) a min(E MAXMAXNAP04RATI0, a na(P04SETPOINT(blr),PHSETPOINT(blr)+DPHSETPOINT(blr), K DEFAULT TEMP,K DEFAULT NH3) / P04SETPOINT(blrj) );. - -/* e-fixNAN: replace E NAN's (not a number) with a given value */
double a fixNAN(double number, double forNAN) {
return (number =- E NAN ? forNAN : number);

/* Main Functions */ C A 215 6 8 8 0 /* .nit: check that required constants of macc4 are properly specified*/
/* mould 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 = famin; bl.famax = ... etc. (define the fields)-if (e init(&bl) != E INITOK) error ( ) ;
*/
E INITSTATUS a init(E BOILER *blr) _ _ blr->initstatus = E INITOK; /* clear initstatus flag */
blr->undoable = E_FALSE;
blr->updatestatus = E UPDATEOK;
if (blr->t =- E blr->initstatus = MISSINGT;
NAN) E

else if (blr->famin E blr->initstatus = MISSINGFAMIN;
=- NAN) E

else if (blr->famax E blr->initstatus = MISSINGFAMAX;
=- NAN) E
~

else if (blr->fadef E blr->initstatus = MISSINGFADEF;
=- NAN) E

else if (blr->po4a -- E blr->initstatus = MISSING
NAN) E P04A;

else if (blr->ratioa== E blr->initstatus = MISSING
NAN) E RATIOA;

else if (blr->fbmin E blr->initstatus = MISSINGFBMIN;
=- NAN) E

else if (blr->fbmax E blr->initstatus = MISSINGFBMAX;
=- NAN) E-else if (blr->fbdef E blr->initstatus = MISSINGFBDEF;
=- NAN) E

else if (blr->po4b -- E blr->initstatus = MISSINGP04B;
NAN) E

else if (blr->ratiob== E blr->initstatus = MISSINGRATIOB;
NAN) E

else if (blr->po4~ -- E blr->initstatus = MISSINGP04;
NAN) E

else if (blr->ph -- E blr->initstatus = MISSINGPH;
NAN) E

else if (blr->m -- E blr->initstatus = MISSINGM;
NAN) E

else if (blr->bdtemp== E blr->initstatus = MISSINGBDTEMP;
NAN) E

else if (blr->po4setpoint - - -E blr->initstatus = MISSINGP04SETPOINT;
NAN) E

else if (blr->minpo4== E blr->initstatus = MISSINGMINP04;
NAN) E

else if (blr->maxpo4== E blr->initstatus = MISSINGMAXP04;
NAN) E
-else if (blr->phsetpoint -- E &&
NAN

blr->napo4rati o -=E _NAN)blr->initstatus E
= MISSING
RATIOSETPOINT;

#if NEERING UNITS - -E
ENGI

else if (blr->spga =- NAN) blr->initstatus E~MISSING SPGA;
E =

else if (blr->spgb =- E blr->initstatus E SPGB;
NAN) = MISSING

#endif - - -r i r. 1 ~;
I ' ~ . / 1 ,~ J~U
v i : ! .: ,.;
else { /* if we got this far, all required fields have been specified */
/* ' Mine initial feed program as feeding the default feed rates */
"'blr->dt [0] - blr->dt [1] - 0.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 = blr->t; /* set origin of time, po4, and ph */
blr->lastbdtemp = blr->bdtemp;
blr->lastpo4 = blr->po4;
blr->lastnh3 = blr->nh3;
blr->lastph = blr->ph;
/* provide defaults for the optional parameters if they are undefined */
blr->fdt = a fixNAN(blr->fdt, 0.0); /*assume instantaneous pump changes*/
blr->dpo4 = a fixNAN(blr->dpo4, DEFAULT DP04);
blr->dph = a ~ixNAN(blr->dph, DEFAULT DPH);
blr->dphsetpoint = a fixNAN(blr->dphsetpoint, DEFAULT DPHSETPOINT);
blr->max sample interval = a fixNAN(blr->max sample interval, DEFAUL'1 MAX SAMPLE INTERVAL);
blr->max outo~box adjustment =
a fixNAN(blr->max outofbox adjustment, DEFAULT_MAX OUTOFBOX ADJUSTMENT);
blr->beps = a fixNAN(blr->beps, DEFAULT BEPS);
blr->naleakmaX = a fixNAN(blr->naleakmaX, 1.0e3 * (FAMAXTblr)*fabs(NAA(blr)) + FBMAX(blr)*fabs(NAB(blr)) +
FAMAX (blr) *fabs (P04A(blr) ) + FBMAX (blr) *fabs (P04B (blr) ) ) ) ;
blr->naleakmin = a fixNAN(blr->naleakmin, - blr->naleakmax);
blr->bdmax = a fixNAN(blr->bdmax, blr->m/1.0);
/*default bdmax empties boiler in 1 hr*/
blr->bdmin = a fixNAN(blr->bdmin, l.Oe-6*blr->m/1.0);
/* defaultf bdmin is blowdown rate needed for a time constant of 1e6 hours */
blr->max-relative error = e_fixNAN(blr=>max relative error, DEFAULT MAX RELATIVE ERROR);

/* apply consistency checks to the constants of the macc4 model */
' if (BDMAX (blr) < BDMIN (blr) ) blr->initstatus = E BDMAX LT BDMIN;
".".lse if (NALEAKMP.X (blr) < NALEAKMIN (blr) ) blr->initstatus = E NALEAFQHAX LT NALEAKMIN;
else if (FAMAX(blr) --FAMIN(blrf <_ DBL EPSILON * (fabs(FAMAX(blr))+fabs(FAMIN(blr)))) blr->initstatus = E_FAMAX LE FAMIN; /* max pump rates less than min */
else if (FBMAX(blr) - FBMINTblr) <_ DBL EPSILON*fabs(fabs(FBMAX(blr))+fabs(FBMIN(blr)))) blr->initstatus.= E FBMAX LE FBMIN; /* max pump rates less than min */
else if (MINP04(blr). < E MINMINP04) blr->initstatus = E MINP04 LT MINMINP04;
else if (MAXP04(blr) > E MAXMAXP04) blr->initstatus = E MAXP04 GT MAXMAXP04;
else if (P04SETPOINT(blr) < MINP04(blr)) blr->initstatus = E P04SETPOINT LT MINP04;
else if (P04SETPOINT(blr) > MAXP04(bIr)) blr->initstatus = E P04SETPOINT_GT_MAXP04;
else if (P04A(blr) < 0.0) blr->initstatus = E P04A LT ZERO;
else if (P04B(blr) < 0.0) -blr->initstatus = E P04B LT_ZERO;
else if (e minnapo4ratio(bIr) < E MINMINNAP04RATI0) blr->initstatus = E MINRATIO LT MINMINRATIO;
else if (e maxnapo4ratio(blr) > E MAXMAXNAP04RATI0) blr->initstatus = E MAXRATIO GT MA)~MAXRATIO;
else if (e napo4ratioZblr) < a minnapo4ratio(blr)) blr->initstatus = E RATIO LT MINRATIO;
else if (e napo4ratioZblr) > a maxnapo4ratio(blr)) blr->initstatus = E RATIO GT MAXRATIO;
else if (fabs(NAA(blrT*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, leak only used for testing--reasonable defaults*/
blr->1 = a fixNAN(blr->1, 0.0);
blr->bd = a fixNAN(blr->bd, USER}D((FADEF(blr~*P04A(blr) + FBDEF(blr)*P04B(blr))/P04SETPOINT(blr)));
return(blr->initstatus);

CA215b880 /* e-getfeed: retrieves feed rates for pumps a or b for a given time */
dou' ' e_getfeed(E BOILER *blr, double *f) double deltat = T(blr) - LASTT(blr);
if (deltat < ROUNDED 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 BOILER*blr) { /* retrieves pump a feed rate */
returnee getfeed(blr; blr->fa));
double a bfeed(E BOILER *blr) { /* retrieves pump b feed rate */
returnee-getfeed(blr, blr->fb));
/*e~o4pred: predicted po4 concentration */
double e~o4pred(E BOILER *blr) {
double po4 == LASTP04(blr);
double deltatime = T(blr) - LASTT(blr);
int i = 0;
double deltat[3];
deltat[0] - a min(deltatime,ROUNDED DT(blr,0));
deltat[1] - a min(deltatime - deltat[0], ROUNDED DT(blr,l));
deltat [2] = deltatime - deltat [0] - deltat [1] ;
for (i = 0; i < 3; i++) {
po4 = ( (FA(blr, i) *P04A(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);

/* P estimate bd: estimate blowdown (B) via bisection */ C A 215 ~ 8 8 0 Doug ' 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-- && (Bhigh - Blow) >
blr->beps*(fabs(blr->bdmax) + fabs(blr->bdmin))) {
if (e o4pred(blr) > P04(blr)) B~w = blr->bd; /* estimated po4 too high - Blowdown too low */
else Bhigh = blr->bd; /* estimated po4 too low - Blowdown too high */
blr->bd = 0.5 * (Blow + Bhigh);
/* flag if the final low, high don't actually bracket the root */
blr->bd = Blow;
if (e o4pred(blr) < P04(blr)) blr->updatestatus ~= E INCONSISTENT P04;
blr->~ = Bhigh;
if (e~o4pred(blr) > P04(blr)) blr->updatestatus ~= E-INCONSISTENT P04;
return(blr->bd = 0.5 * (Blow + Bhigh));
/*e napred: predicted na concentration */
double a napred(E BOILER *blr) {
double na = ewa(LASTP04(blr), LASTPH(blr), LASTBDTEMP(blr), LASTNH3(blr));
double deltatime = 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 [0] , ROUNDED DT (blr, 1) ) ;
deltat [2] = deltatime - deltat [0] -' deltat (1] ;
for (i=0; i < 3; i++) {
na = ( (FA(blr, i) *NAA(blr) + FB (blr, i) *NAB (blr) +L (blr) ) /BD (blr) ) ( 1. 0-exp ( - deltat [i] *BD (blr) /M (blr) ) ) +
na*exp ( - deltat [i] *BD (blr) /M (blr) ) ;
return(na);

/* a estimate_l : estimate na "leak" (L) via bisection */ C Q 215 6 8 8 0 dou ' e_estimate 1(E BOILER *blr) int bisections = E MAXBISECTIONS;
double na = a na(P04(blr), PH(blr), BDTEMP(blr), NH3(blr));
double Llow = bIr->naleakmin;
double Lhigh = blr->naleakmax;
blr->1 = 0.5*(Llow+Lhigh);
while (bisections-- && (Lhigh - Llow) >
blr->beps*(fabs(blr->naleakmax) + fabs(blr->naleakmin))) {
if (e napred(blr) > na) /* estimated na too high - na Leak too high */
Lhigh = blr->1;
else /* estimated na too low - na leak too low */
Llow = blr->1;
blr->1 = 0.5*(Llow+Lhigh);
/* fl}g 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 = Lhigh;
if (e napred(blr) < na) blr->updatestatus ~= E-INCONSISTENT-PH;
return(blr->1 = 0.5*(Llow+Lhigh));
/* eTphmodel: computes the current model predicted pH for given boiler.*/
#define LOGBASE20FRELATIVEPHERROR 64 /*works out to less than 1e-16 pH units*/
double e_phmodel(E BOILER *blr) {
double minph = 1.0;
double maxph = 14.0;
double ph = 0.5*(minph + maxph);
double po4 = e_po4pred(blr);
double na - a 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~o4model: computes the current model predicted po4 for given boiler*/
double erpo4model(E BOILER *blr) {
return (USERP04(e~o4pred(blr)));

/* Note on our representation of convex polygon regions : C Q 215 6 8 8 0 ~nvex polygon regions with N vertexes, p1, p2, ... pn are represented by _...1 points, R[0] , R[1] , .. ., R[N] as follows:
R [0] - p1; R [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). */
/* erperimetercrossings: determines points where a given line intersects the perimeter of a given convex polygon region; returns number of intersection point s */
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 VECTOR *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), a rot90(e vSv(vP2,vP1)), &lastalpha, &last{eta);
for ~(i=0; i < NV; i++) a decompose(&vV[i+1], vPl, e_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 < 2 ) vX [N++] - *X;
else if (e between (&vX [1] , &vX [0] , X) ) vX [0] - *X;
else if (e between (&vX [0] , &vX [1] , X) ) vX [1] - *X;
}* else first 2 intersection points are already the furthest apart */
lastbeta = beta;
return(N);

/* e-within region: is the given point within (or on perimeter of) region ? */
E_T~~iICAL a within_region(E_VECTOR *vP, E_VECTOR *vR, int N) 'ECTOR vX [ 2 ] ;
i'~ ( a vEv ( vP , &vR [ 0 ] ) ) return(E TRUE);
else if (e~erimetercrossings(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, tfiat 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) */
int N, /* number of distinct points on perimeter */
E VECTOR *vPl, E VECTOR *vP2) { /* clipping line */
double alpha, beta, lastbeta;
int i = 0, j - o;
a decompose(&vR[0], vPl, a vSv(vP2, vPl), a rot90(e vSv(vP2, vPl)), &alpha, &b{ta);
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, vPl)), &alpha, &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[j1 - vC[0]; /* "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 H. The ~-lrst 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 *vAB, /* intersecting region (returns NAB) */
E VECTOR *vA, /* points representing the 1st region */
int NA, /* number of vertexes in 1st region */
E VECTOR *vB, /* points representing the 2nd region */
int NB) { /* number of vertexes in 2nd region */
int i = 0, j - 0, N = NB;
for (i = 0; i < NA; i++) {
for (j = N; j >= 0; j--) { /* co y/move region to be clipped to end of AH*/
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]);
}eturn(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) {
if (e vEv(vLl, vL2)) return(vLl);
else 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 vSv(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_~s..,...TOR *e region2region(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~oint2chord (&vA [i] , &vB [j ] , &vB [j+1] ) ;
if (e norm(e vSv(&vA[i], &vX)) < a norm(e vSv(&vminA, &vminB))) {
vminA = vATi];
vmins = vX;
for (i=0; i < NB; i++) {
for (j = 0; j < NA; j++) {
vX = *e~oint2chord(&vB[i] , &vA[j] , &vA[j+1] ) ;
if (e norm(e vSv(&vX, &vB[i])) < a norm(e vSv(&vminA, &vminB))) {
vminA = vX;
vminB = vB [ i ] ;
}eturn(e vec(vminA.po4,vminA.na));
# define E REPS 5 /* number of replicates for sensitivity analysis */
/* e~ertubate: perturbs field in boiler structure for sensitivity analysis */
void e~erturbat{(E BOILER *blr, int index, double multiplier) {
switch(index) case 0: blr->lastpo4 +_ (multiplier*blr->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 */
vo' a backup(E BOILER *blr) i,s.~. 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;
blr->b4lastbdtemp =.blr->lastbdtemp;
blr->b4lastpo4 - blr->lastpo4;
blr->b4lastnh3 - blr->lastnh3;
blr->b4lastph - blr->lastph;
blr->lastbd - blr->bd;
blr->lastl - blr->1;
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];
blr->lastupdatestatus = blr->updatestatus;
blr->undoable = E TRUE; /* since we are backed up, an undo is now possible */
%* a 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);
else {
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->lastfa [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 < E NV+1; i++) blr->vV[i] - blr->vlastV[i];
for (i=0; i < E NW+1; i++) blr->vW[i] - blr->vlastW[i] ;
for (i=0; i < 3; i++) blr->vF [i] - blr->vlastF [i] ;
blr->updatestatus = blr->lastupdatestatus;
blr->undoable = E_FALSE; /* no more than 1 undo between updates */
return(E TRUE);

/* a update: update the feed program associated with a boiler */
E_L ATESTATUS a update(E BOILER *blr) int i = 0 , i 0 = 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, vSS;
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 */
bIr->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 the clockwise direction, c.f. comment at top of e_intersect regions() */
vW[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));
vW [3 ] - *e vec (MAXP04 (blr) , MAXP04 (blr) *e-minnapo4ratio (blr) ) ;
vw [ 4 ] - vwlo ] ;
/* ... and target point */
vT = *e vec(P04SETPOINT(blr), P04SETPOINT(blr)*e napo4ratio(blr));
/* Estimate the steady-state boiler concentration variability due to */
/* the 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 */
/* boiler concentrations. But it should be a reasonable estimate. */
i0 = ((DP04(blr)<=0.0 && DPH(blr) <= 0.0') ? (E REPS-1) ~ 0);
for (i = i0; i < E REPS; e~perturbate(blr, i++, -1.0)) /* this loop evaluates steady-state feed rates using pertubated inputs */
a~ erturbate(blr, i, 1.0); /* varies one of: lastpo4,lastph,po4,ph *~
bIr->bd = a estimate bd(blr); /* (if i = E REPS-1 then it varies nothing)*i blr->1 - e-estimate 1(blr);
vfamin = *e vec (FAMIN (blr) *P04A (blr) /BD (blr) , FAMIN(blr)*NAA(blr)/BD(blr));
vfbmin = *e vec (FBMIN (blr) *P04B (blr) /BD (blr) , FBMIN (blr) *NAB (blr) /BD (blr) ) ;
vfamax = *e vec(FAMAX(blr)*P04A(blr)/BD(blr), FAMAX (blr) *NAA (blr) /BD (blr) ) ;
vfbmax = *e vec(FHMAX(blr)*P04B(blr)/BD(blr), FBMAX (blr) *NAB (blr) /BD (blr) ) ;
vL - *e vec (0 . 0, L (blr) /BD (blr) ) ;
a decompose (&vT, &vL, a vDs(&vfamax, FAMAX(blr)), a vDs (&v~bmax, FBMAX (blr) ) , &fa [i] , &fb [i] ) ;

'for (ssq = 0.0, i = i0; i < E REPS-1; i++) { /* sum up the squared errors*/
ssq += a norm2( - /* on steady-state boiler */
a vSv(&vT, /* concentrations due to */
- a vPv(&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 INDETERMINATE; /* variability of steady-state */
blr->updatestatus T= 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(&vL, a vPv(&vfamin,&vfbmin));

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

/* ... d the current an point */

vP - *e vec(P04(blr),a na(P04(blr),PH(blr), BDTEMP(blr), NH3(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 ((NV'W=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 0] = 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 (= E BOX_UNMAINTAINABLE;
vSS = *e region2regionZvV, NV, vW, NW);
blr->dt[0] - blr->dt[1] - 0.0; /* set default feed program to */
vF[0] - vF[1] - vF[2] - vSS; /* steady state feed rates */

/* Adjust na/po4 ratio (stage 1 of feed program) */ ~ ~ ~ 15 6 8 8 0 - (E FALSE==a within_region(&vP, vW, NW)) ( /* if already in the box */
,_ int 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_perimetercrossings(vX, vW, NW, &vP, &vV[i]) > 1) {
for (j=0; j < 2; j++) { &vV [i] ) ) if (e between (&vP, &vX [j ] , {
if region unreachable) vminV = vV [ i ] ;
vminW = vX [j ] ;
region_unreachable = E_FALSE;
else if (e norm (e vSv(&vV[i] ,&vX[j] ) ) *e norm(e vSv(&vminV,&vP) ) >
e-norm(e-vSv(&vminV,&vminW))*e-norm(e-vSv(&vV[i],&vP))) {
vminV = vV [i] ;
vminW = vX [j ] ;
} }
} }
for (i=0; i < NW; i++) {
i f ( ! a 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 Zregion 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(e-vSv(&vminV,&vminW)) *e norm(e vSv(&vX[j],&vP))){
vminV = vX[j];
vminw = vw [ i ] ;
} }
} }
}f (region unreachable==E TRUE
(e norm(e vSv(&vminV,&vminW)) <_ .
2 0*DBL MIN*e norm(e vSv(&vminV, &vP)))) blr{>updatestatus ~= E BOX UNREACHABLE;
else blr->dt [0] - - (M(blr) /BD(blr) ) loge norm(e vSv(&vminV,&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 setpoint po4 and NA/po4 ratio if you can */
i f ,- vEv ( &vP , &vT ) ) it'--te~ erimetercross~n s(vX, vV, NV, &vP, &vT) <= 1) b{r->updatestatus ~= E_POINT-UNREACHABLE;
else int i = 0;
E LOGICAL point unreac{able = E TRUE;
for (i=0; i < 2; i++) if (e between(&vP, &vT, &{X[i])) {
if Zpoint unreachable) vminV = vX [ i ] ;
point_unreachable = E_FALSE;
else 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 ] ;
if (E TRUE==point unreachable a norm(e vSv(&vminV,&vT)) <= 2.0*DBL MIN*e norm(e vSv(&vminV, &vP))) bIr->updatestatus (= E_POINT_UNREACHABLE;
else blr{ >dt [1] - - (M (blr) /BD (blr) ) log(e_norm(e_vSv(&vminV,&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,blr->fbmax) , &blr->fa [i] , &blr->fb [i] ) ;
blr->fa [i] - a forceinrange (blr->fa [i] , b'lr->famin, blr->famax) ; /*fixes up*/
blr->fb[i] - a forceinrange(blr->fb[i],blr->fbmin,blr->fbmax);/*roundoff*/
if (T(blr) - LASTT(blr) > MAX SAMPLE INTERVAL(blr)) blr->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;
nochange: /* if we jump to here, feed program not updated*/
return(blr->updatestatus);

con~t E
BOILER
*e undefined boiler (void) R

s'catic const E
BOILER
E_UNDEFINED
BOILE
=
{

E_N~'- NAN,E NAN,E NAN
E' NAN,E
NAN,E
NAN,E
NAN,E
NAN,E_NAN,E
NAN,E
NAN,E

, E_NA.~ NAN,E NAN,E
~ NAN,E NAN
NAN,E
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 NAN
NAN,E
NAN,E
NAN,E_NAN,E
NAN,E
NAN,E

_ _ E , NAN,E NAN,E NAN,E NAN
NAN,E E NAN
NAN,E
NAN,E
NAN,E
NAN,E
NAN,E
NAN,E
NAN,E

_ , 0.0,0.0} ,{E ,{E }, , NAN,E NAN,E - - -NAN,E NAN,E
NAN} NAN

0.0,0.0} ( E , E , NAN,E NAN,E
NAN,E NAN
NAN} E
NAN}

0.0,0.0 _ , 0.0,0.0 , 0.0,0.0_ 0.0,0.0 , , 0.0,0 , 0.0,0.0 0.0,0.0 , 0.0,0.0 , 0.0,0.0, 0.0,0.0 , , 0.0,0.0 0.0,0.0 , 0.0,0.0 , 0.0,0.0, 0.0,0.0 , , 0.0,0.0 0.0,0.0 , 0.0,0.0 0.0,0.0, 0.0,0.0 , , 0.0,0.0 0.0,0.0 , 0.0,0.0 ~
, ;
0.0,0.0 0.0,0.0 , 0.0,0.0 , E
UPDATEOK, E
UPDATEOK, E
INITOK, E
FALSE};

}eturn(&E
UNDEFINED
BOILER);

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

/* macc4ssp.h */
#def~a IDC PASSWORD 155 #defineIDC LOCKWINDOW 156 #defineIDC DATETIME 157 #defineMACC4SSP 2 ICON

#defineMACC4SSP 1 DIALOG

#defineIDC GROUPBOX1 101 #defineIDC BD 152 #defineIDC BDMIN 131 #defineIDC BDMAX 132 #defineIDC M 130 #defineIDC BDTEMP 127 #defineIDC DPHSETPOINT126 #defineIDC PHSETPOINT 125 #defineIDC DPH 124 #defineIDC PH 123 #defineIDC MAXP04 120 #defineIDC MINP04 119 #defineIDC P04SETPOINT118 #defineIDC DP04 117 #defineIDC P04 116 #defineIDC SPGA 146 #defineIDC SPGB 114 #defineIDC P04B 112 #defineIDC FBDEF 111 #defineIDC FBMAX 110 #defineIDC FBMIN 109 #defineIDC RATIOB 107 #defineIDC RATIOA 106 #defineIDC P04A 105 #defineIDC FADEF 104 #defineIDC STATUSFLAGS151 #defineIDC POINTNAMES 154 #defineIDC START 121 #defineIDC SIMULATESSP147 #defineIDC NH3ENABLED 162 #defineIDC NH3TEXT 163 #defineIDC-NH3 164 #defineIDC AFEED 200 #defineIDC BFEED 201 #defineIDC FAMAX 103 #defineIDC FAMIN 102 #defineIDC GROUPBOXS 129 #defineIDC MESSAGELINE 145 #defineIDC LOGTOFILE 113 #defineIDC GROUPBOX6 148 #defineIDC ENGINES 144 #defineIDC ENGINEB 143 #defineIDC ENGINE? 142 #defineIDC ENGINE6 141 #defineIDC ENGINES 140 #defineIDC ENGINE4 139 #defineIDC ENGINE3 138 #defineIDC ENGINE2 137 #defineIDC ENGINE1 136 #defineIDC ENGINEO 135 #defineIDC STOP 122 #defineIDC UNDO 204 #defineIDC REINIT 203 #defineIDC UPDATE 202 #defineIDC LOCALINPUT 158 #defineIDC FORECASTTIME149 #def IDC FORECAST 150 C A 21 5 6 8 ine IDC GROUPBOX4 -1 8 0 #deine #defineIDC GROUPBOX3 115 #def. IDC GROUPBOX2 108 ' #def. IDC FWFLOW 128 #defineIDC FWNA 161 #defineIDC MAXFWNA 160 #defineIDC MINFWNA 159 #define IDC AFEED 200 /* SSP communication id's */
#define IDC BFEED 201 /*(IDC-PH, IDC-P04, and IDC BDTEMP also used)*/
#define IDC FLAG1 205 #define IDC FLAG2 206 #define IDC FLAGS 207 #define IDC FLAG4 208 #define IDC FLAGS 209 #define IDC FLAG6 210 #define IDC FLAG7 211 #define IDC FLAGS 212 #define IDC FLAG9 213 #define IDC FLAG10 214 #define IDC FLAG11 215 #define IDC FLAG12 216 #define IDC RATIO 217 #define IDC T 300 /* E BOILER field ids that do not have */
#define IDC NAP04RATI0 301 /* a control associated with them */
#define IDC MINNAP04RATI0 302 #define IDC MAXNAP04RATI0 303 #define IDC MAX SAMPLE INTERVAL 304 #define IDC MAX OUTOFBOX ADJUSTMENT 305 #define 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 #define IDC LASTPH 312 #define IDC B4LASTT 313 #define IDC B4LASTBDTEMP 314 #define IDC B4LASTP04 315 #define IDC B4LASTPH 316 #define IDC LASTBD 319 #define IDC LASTL 320 #define IDC DT1 321 #define IDC DT2 322 #define IDC FA1 323 #define IDC FA2 324 #define IDC FA3 325 #define IDC FB1 326 #define IDC FB2 327 #define IDC FB3 329 #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 LASTFH3 336 #define IDC UPDATESTATUS 337 #define IDC LASTUPDATESTATUS 338 #define IDC INITSTATUS 339 #define IDC UNDOABLE 340 #define IDC LASTNH3 341 #define IDC B4LASTNH3 342 /*,'misc/MACC4SSP specific/state/control/error ids */
#dea IDC CURRENTENGINEID 401 C A~ 1.~~88a #de W a IDC RUNNING 402 #define IDC FLAGSUPTODATE 403 #define IDC ACTIONSUPTODATE 404 #define IDC SCIERR 405 #define IDC PIDSCIERR 406 #define IDC ACTION 407 #define IDC CHECKSUM 408 #define IDC LOADFORM 409 #define IDC LASTEVENTTIME 410 #define IDC EVENTID 411 #define IDC STARTTIME 412 #define IDC LAUNCH 413 #define IDC CLOSE 414 r'!~~,2~ ~b3~Q
****************************************************************************
mac~.~sp . rc produced by Borland Resource Workshop *****************************************************************************, #define IDC PASSWORD 155 #define IDC FWNA 161 #define IDC MAXFWNA 160 #define IDC MINFWNA 159 #define IDC LOCKWINDOW 156 #define IDC DATETIME 157 #define MACC4SSP ICON 2 #include <windows.h>
#define MACC4SSP DIALOG 1 #define IDC GROUPBOX1 101 #define IDC BD 152 #define IDC BDMIN 131 #define IDC BDMAX 132 #define IDC M 130 #define IDC BDTEMP 127 #define IDC DPHSETPOINT 126 #define IDC PHSETPOINT 125 #define IDC DPH 124 #define IDC PH 123 #define IDC MAXP04 120 #define IDC MINP04 119 #define IDC P04SETPOINT 118 #define IDC DP04 117 #define IDC P04 116 #define IDC NH3TEXT 163 #define IDC NH3 164 #define IDC SPGA 146 #define IDC SPGB 114 #define IDC P04B 112 #define IDC FBDEF 111 #define IDC FBMAX 110 #define ~IDC FBMIN 109 #define IDC RATIOB 107 #define IDC RATIOA 106 #define IDC P04A 105 #define IDC FADEF 104 #define IDC STATUSFLAGS 151 #define IDC POINTNAMES 154 #define IDC NH3UNDERLINE 165 #define IDC START 121 #define IDC SIMULATESSP 147 #define IDC AFEED 200 #define IDC BFEED 201 #define IDC FAMAX 103 #define IDC FAMIN 102 #define IDC GROUPBOX5 129 #define IDC MESSAGELINE 145 #define IDC LOGTOFILE 113 #define IDC NH3ENABLED 162 #define IDC FWFLOW 128 #define IDC GROUPBOX6 148 #define IDC ENGINES 144 #define IDC ENGINEB 143 #define IDC ENGINE? 142 #define IDC ENGINE6 141 #define IDC ENGINES 140 #de,fine IDC ENGINE4 139 #define IDC ENGINE3 138 -#def . IDC ENGINE2 137 #def~zze 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, 1, 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 CONTROL "Last update:", -1, "STATIC", SS LEFTNOWORDWRAP ~ WS CHILD ~ WS
VISIBLE
CONTROL "24-Dec-1994 09:24:29", IDC DATETIME, "STATIC", SS LEFTNOWORDWRAP ~
WS_ GROUPHOX "Pump/Tank A", IDC GROUPBOX1, 4, 14, 109, 91, BS GROUPBOX
LTEXT "Min Feed (gpd):", -1, 12, 30, 52, 12 EDITTEXT IDC FAMIN, 70, 28, 40, 12 CONTROL "Max Feed (gpd):", -1, "STATIC", SS LEFTNOWORDWR,AP ~ WS CHILD ~
WS_VISI
EDITTEXT IDC FAMAX, 70, 40, 40, 12 CONTROL "Init. Feed (gpd):", -1, "STATIC", SS LEFTNOWORDWRAP ~ WS CHILD ~
WS_VI
EDITTEXT IDC FADEF, 70, 52, 40, 12 LTEXT "P04 (a by wt.):", -1, 12, 66, 53, 12 EDITTEXT IDC P04A, 70, 64, 40, 12 LTEXT "Na/P04 ratio:", -1, 12, 78, 52, 12 EDITTEXT IDC RATIOA, 70, 76, 40, 12 LTEXT "Specific Gravity:", -1, 12, 90, 55, 12 EDITTEXT IDC SPGA, 70, 88, 40, 12 GROUPBOX "Pump/Tank B", IDC_GROUPBOX2, 118, 15, 109, 90, BS GROUPBOX
CONTROL "Min Feed (gpd):", 1, "STATIC", SS LEFTNOWORDWRAP T WS CHILD ~ WS VISI
EDITTEXT IDC FBMIN, 183, 28, 40, 12 CONTROL "Max Feed (gpd):", -1, "STATIC", SS LEFTNOWORDWRAP ~ WS CHILD ~
WS_VISI
EDITTEXT IDC FBMAX, 183, 40, 40, 12 CONTROL "Init. Feed (gpd):", -1, "STATIC", SS LEFTNOWORDWRAP ~ WS CHILD ~
WS_VI
EDITTEXT IDC FBDEF, 183, 52, 40, 12 LTEXT "P04 (% by wt.):", -1, 127, 64, 52, 12 EDITTEXT IDC P04H, 183, 64, 40, 12 LTEXT "Na/P04 ratio:", -1, 127, 76, 49, 12 EDITTEXT IDC RATIOB, 183, 76, 40, 12 LTEXT "Specific Gravity:", -1, 127, 88, 56, 12 EDITTEXT IDC SPGB, 183, 88, 40, 12 GROUPBOX "Boiler Phosphate", IDC GROUPBOX3, 4, 109, 109, 91, BS GROUPBOX
CONTROL "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, 12 LTEXT "Set Point (ppm):", -1, 10, 147, 55, 9 EDITTEXT IDC P04SETPOINT, 70, 146, 40, 12 LTEXT "Min (ppm):", -1, 10, 159, 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 ~ WS CHILD ~ WS_VISIBLE ~ WS
GRC
EDITTEXT IDC PH, 184, 123, 40, 12 LTEXT "Variability:", -1, 128, 137, 38, 12 ED.ITTEXT IDC DPH, 184, 135, 40, 12 L'IEXT "Setpoint:", -1, 128, 150, 30, 10 EDI~"~EXT IDC PHSETPOINT, 157, 149, 31, 13 LTA. "+/-",--1, 188, 151, 11, 9 L " ' EDI~!'EXT IDC DPHSETPOINT, 199, 149, 25, 13 EDITTEXT IDC HDTEMP, 184, 164, 40, 12 CHECKBOX " ", IDC NH3ENABLED, 119, 176, 9, 11, BS AUTOCHECKBOX ~ WS TABSTOP
CONTROL "NHS (ppm)*:", IDC NH3TEXT, "STATIC", SS LEFTNOWORDWRAP ~ WS CHILD ~
WS
EDITTEXT IDC NHS, 184, 176, 40, 12 - -CONTROL "", IDC NH3UNDERLINE, "static", SS BLACKFRAME ~ WS CHILD ~ WS VISIBLE
LTEXT "* of boiler water pH sample", -1, 120, 190, 106, 9 - -GROUPBOX "Other Parameters", IDC GROUPBOXS, 231, 109, 117, 91, BS GROUPBOX
CONTROL "BW Mass (lbs):", -1, "STATIC", SS_LEFTNOWORDWRAP ~ WS CHILD ~ WS
VISIB
EDITTEXT IDC M, 297, 122, 49, 12 -CONTROL "FW Flow (lbs/hr):", -1, "STATIC", SS LEFTNOWORDWRAP ~ WS CHILD ~ WS
VI
EDITTEXT IDC FWFLOW, 297, 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 LEFTNOWORDWRAP WS CHILD WS VISIBLE WS GR
CONTROL " BD (lb/hr)", -1, "STATIC", SS_LEFTNOWORDWRAP I WS CHILD ~ WS VISIBLE
EDITTEXT IDC BDMIN, 248, 159, 49, 12 EDITTEXT IDC BDMAX, 248, 171, 49, 12 EDITTEXT IDC BD, 248, 183, 49, 12 LTEXT "FW Na (ppb)", -1, 297, 150, 49, 10, WS BORDER ~ WS GROUP
EDITTEXT IDC MINFWNA, 297, 159, 49, 12 EDITTEXT IDC MAXFWNA, Z97, 171, 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 "Boilerl", IDC ENGINE1, 36, 217, 35, 14 PUSHBUTTON "Boiler2", IDC ENGINE2, 71, 217, 35, 14 PUSHBUTTON "Boiler3", IDC ENGINES, 106, 217, 35, 14 PUSHBUTTON "Boiler4"; IDC ENGINE4, 141, 217, 35, 14 PUSHBUTTON "Boiler5", IDC ENGINES, 176, 217, 35, 14 PUSHBUTTON "Boiler6", IDC ENGINE6, 211, 217, 35, 14 PUSHBUTTON "Boiler?", IDC ENGINE?, 246, 217, 35, 14 PUSHBUTTON "Boiler8", IDC ENGINE8, 281, 217, 35, 14 PUSHBUTTON "Boiler9", IDC ENGINES, 316, 217, 35, 14 CONTROL "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 CHECKBOX "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 UNDO, 270, 35, 39, 12 PUSHBUTTON "Reinit", IDC REINIT, 309, 35, 39, 12 GROUPHOX "Outputs to SmartScan+", IDC GROUPBOX6, 231, 48, 117, 44, BS_GROUPBOX
LTEXT "Feed Rate A (gpd):", -1, 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, 8 LTEXT "", IDC STATUSFLAGS, 292, 78, 51, 10, WS_BORDER ~ WS_GROUP
EDITTEXT IDC FORECASTTIME, 231, 94, 17, 12 PUSHBUTTON "fir P04/pH forecast", IDC FORECAST, 248, 94, 66, 12 CHECKBOX "Logfile", IDC LOGTOFILE, 316, 95, 32, 12, BS AUTOCHECKBOX ~ WS
TABSTC
CONTROL "", -1, "static", SS BLACKFRAME WS CHILD WS VISIBLE, 128, 133, 11, CONTROL "", -1, "static", SS_BLACKFRAME WS CHILD WS_VISIBLE, 233, 192, 12, CONTROL "", -1, "static", SS_BLACKFR.AME WS CHILD WS VISIBLE ~ WS_BORDER, 11 CONTROL "", -1, "static", SS_BLACKFRAME WS CHILD WS VISIBLE WS BORDER, 12 CONTROL "Temperature (F)*:", -1, "STATIC", SS LEFTNOWORDWRAP ~ WS CHILD ~
WS_VI
GROUPHOX "Boiler pH", IDC GROUPBOX4, 118, 109, 109, 91, BS GROUPBOX

/*,inacc4ssp.c: Model Adaptive Congruent Controller Engine for SmartScan Plus*/
/* version 1Ø Copyright (c) Betz Labs, 1994. All rights reserved. */
/* ws-~.tten by John Gunther, R&D Computer Applications Group, July, 1994 */
/* (extension 2760, Trevose office) */
#include <windows.h>
#include <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 fields, other parameters */
#define TESTING 0 /* determines the test pattern used in operator sim.*/
#define WORKAROUND 0 /* used to workaround memory leak in SCIL 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 "°s.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 NANSTRING "" /* 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") */
BP TRUE=TRUE, BP_FALSE=FALSE, BP_NANBOOL=NANBOOL, HOOLPLUS;
typedef struct simsspevent { BOOLPLUS update, undo, reinit; ~ SIMSSPEVENT;
{IMSSPEVENT simsspinputs(] -#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}, };

r i stat SIMSSPEVENT nullevent = {FALSE, FALSE, FALSE};

#define MINENGINEID 0 #define DEFENGINEID 0 #define MAXENGINEID 9 typedef 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 Aids) */

typedef struct m4sengine /*in memory representation of { a MACC4SSP Engine */

BOOL running; /*is the engine running? */

SCIError scierr; /*lst 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/0 */

BOOL badchecksum; /* signals a corrupted INI file (invalid state)*/

BOOL logtofile; /* log actions to *.LOG file?*/

BOOL simulatessp; /* simulate SSP inputs? (for testing)*/

BOOL localinput; /* accept inputs locally?*/

BOOL nh3enabled; /* use an ammonia correction pH's?*/
for double starttime; /* (actual, real world) time hich run began at w */

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 na leak */

E BOILER blr; /* MACC4 controller as defined in macc4.h */

M4SENGINE;
M4SENGINE m4e [MAXENGINEID+1] ;
static int currentengineid = DEFENGINEID;
#define xquote(x) #x ' #define quote(x) xquote(x) /* forces expansion of x before quoting it */
# define M SAY(s) MessageBox(hmainwin,s,"Informational message",MB OK) # define M BUGALERT(s) MessageBox(hmainwin,s,\
"MACC4SSP bug in file: " quote( FILE ) " at line: " quote( LINE ),MB_OK) #define M ASSERT(cond) \
if (!(con3)) MessageBox(hmainwin, "Failed assertion: " #cond,\
"Assertion 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) #define M M4SFATALERROR(s) \
Messa eHox(hmainwin, s, "MACC4SSP fatal error--cannot continue.",\
MB OK~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 P04SETPOINT: required field po4setpoint has not been specified", IDC P04SETPOINT}, {"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 FBMIN}, {"E MINP04 LT MINMINP04: lower boiler po4 control limit, minpo4 < 1 ppm", IDC MINP04}, ' {"E MAXP04 GT MA~CMAXP04: upper boiler po4 control limit, maxpo4 > 100 ppm", IDC MAXP04}, {"E P04SETPOINT LT MINP04: setpoint boiler po4 < lower control limit", IDC P04SETPOINT}, ' {"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: setpoint 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 (specific gravity) not specified", IDC SPGB}
}1 typedef enum controlsecuritylevel NULL LEVEL=0, /* no security enforced on the control */
MANAGERONLY LEVEL=1, /* access only for users who have entered password */
OPERATORONLY LEVEL=2,/* access only for users who have NOT entered password*/
CONTROLSECURITYLEVEL;
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 */
NF 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 ? */
CONTROLACCESS 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,"currentengineid",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS,NULL CONTROL}, IDC RUNNING,"running",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, ~IDC FLAGSUPTODATE,"flagsuptodate",NANFLT,NANFLT,NULL LEVEL,NULL_ACCESS, 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 LASTEVENTTIME,"lasteventtime",NANFLT,NANFLT,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, STOREDEDIT 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, NULL CONTROL}, IDC STARTTIME,"starttime",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, ~IDC LOGTOFILE,"logtofile",NANFLT,NANFLT,MANAGERONLY LEVEL,STATIC_ACCESS, STOREDCHECKBOX CONTROL}, {IDC SIMULATESS$,"simulatessp",NANFLT,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDCHECKBOX CONTROL}, {IDC LOCALINPUT,"localinput",NANFLT,NANFLT,MANAGERONLY LEVEL,STATIC_ACCESS, STOREDCHECKBOX CONTROL}, {IDC MINFWNA,"minfwna",NANFLT,NANFLT,MANAGERONLY LEVEL,STATIC_ACCESS, STOREDEDIT CONTROL}, {IDC MAXFWNA,"maxfwna",NANFLT,NANFLT,MANAGERONLY LEVEL,STATIC_ACCESS, STOREDEDIT CONTROL}, caz ~ 56sso {IDC FWFLOW,"fwflow",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, -{IbC BDMIN,"bdmin",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, ST' ~ ;DEDIT CONTROL} , - -{IDS...,3DMAX,"bdmax",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -~IDC M,"m",SMALLNUN1BER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, {IDC DPHSETPOINT,"d hsetpoint",SMALLNUMBER,1.O,MANAGERONLY LEVEL, STATIC
ACCESS, STOREDEDIT CONTROL, - -{IDC PHSETPOINT,"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, STOREDEDIT CONTROL}, {IDC MINP04,"minpo4",1.,1000.,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC P04SETPOINT,"po4setpoint",1.,1000.,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC DP04,"3po4",SMAI,LNUMBER,lO.,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC SPGA,"spga",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC SPGB,"spgb",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC P04B,"po4b",O,100,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, -{IDC FBDEF,'~fbdef",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC FBMAX,"fbmax",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC 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, STOREDEDIT CONTROL}, - -{IDC FAMAX,'~famax",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC FAMIN,"famin",O,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, - -{IDC MESSAG~LINE,"messageline",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL}, - -{IDC NH3ENABLED,"nh3enabled",NANFLT,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDCHECKBOX CONTROL}, - -{IDC NH3TEXT,"nfi3text",NANFLT,NANFLT,NULL LEVEL,NH3 ACCESS,NULL CONTROL}, {IDC START,"START",NANFLT,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, NULL CONTROL}, - -{IDC STOP,"STOP",NANFLT,NANFLT,MANAGERONLY 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,"BDTEMP",32,212, NULL LEVEL, LOCAL ACCESS, STOREDEDIT CONTROL}, - -{IDC P04,"P04",SMALLNUNIBER,l000.0,NULL LEVEL, LOCAL ACCESS, STOREDEDIT CONTROL}, - -{IDC NH3,"I~H3",0.0 1000.0, NULL LEVEL, LOCAL ACCESS~NH3 ACCESS, STOREDEDIT CONTROL , - - -{ IDC PH, "PH" , 1, 14, NULL LEVEL, LOCAL ACCESS, C A 215 6 8 8 ~
STOREDEDIT CONTROL}, {IDC_FWNA,"FWNA",NANFLT,NANFLT,MANAGERON'LY LEVEL, STATIC ACCESS, STOR :DIT CONTROL}, {IDC''rrD,"BD",SMALLNUMBER,NANFLT,MANAGERONLY LEVEL, STATIC ACCESS, STOREDEDIT CONTROL}, {IDC UPDATE,"UPDATE",NANFLT,NANFLT, NULL LEVEL, LOCAL ACCESS~DYNAMIC ACCESS, NULL CONTROL}, {IDC REINIT,"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 HFEED,"BFEED",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC RATIO,"RATIO",SMALLNUMBER,NANFLT, NULL LEVEL, NULL ACCESS, NULL CONTROL}
IDC FLAG1,"FLAG1",NANFLT,NANFLT,NU'LL LEVEL,NULL ACCESS,NU'LL CONTROL , IDC FLAG2,"FLAG2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAG3,"FLAG3",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAG4,"FLAG4",NANFLT,NANFLT,NU'LL LEVEL,NU'LL~ ACCESS,NULL CONTROL , IDC FLAGS,"FLAGS",NANFLT,NANFLT,NUI~L LEVEL,NU'LL ACCESS,NULL CONTROL , IDC FLAG6,"FLAG6",NANFLT,NANFLT,NU'LL LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAG?,"FLAG?",NANFLT,NANFLT,NULL LEVEL,NULL~ACCESS,NULL CONTROL , IDC FLAG8,"FLAGB",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAG9,"FLAGS",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAG10,"FLAG10",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NU'LL CONTROL , IDC FLAG11,"FLAG11",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FLAG12,"FLAG12",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL-CONTROL , /* Just E BOILER fields */
{IDC T,"t",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, {IDC NAP04RATI0,"napo4ratio",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL}, ' {IDC MINNAP04RATI0,"minnapo4ratio",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL}, {IDC MAXNAP04RATI0,"maxnapo4ratio",NANFLT,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL}, {IDC MAX SAMPLE INTERVAL,"max sample interval",NANFLT,NANFLT, NULL LE~EL,NU'L~ ACCESS, NULL CONTROL, {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,NANFLT,NULL LEVEL, NULL ACCESS, NULL CONTROL), IDC FDT,"fdt",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC LASTT,"lastt",NANFLT,NANFLT,NULL 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,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC LASTPH,"lastph",NANFLT,NANFLT,NULL L~VEL,NULL ACCESS,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,NU'LL CONTROL}
IDC B4LASTNH3,"b4lastnh3",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTRO }L
IDC B4LASTPH,"b4lastph",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC LASTBD,"lastbd",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, IDC LASTL,"lastl",NANFLT,NANFLT,NUI~L LEVEL, NULL ACCESS, NULL CONTROL), IDC DT1,"dtl",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC DT2,"dt2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FA1,"fal",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC FA2,"fat",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NU'LL CONTROL , IDC FA3,"fa3",NANFLT,NANFLT,NU'LL LEVEL,NULL ACCESS,NULL CONTROL , ca~~ 5~sso IDS FB1,"fbl",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDG: FB2,"fb2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC ~'B3,"fb3",NANFLT,NANFLT,NULhLEVEL,NULL ACCESS,NULL CONTROL , IDC ~STDT1,"lastdtl",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC~'"LASTDT2,"lastdt2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC LASTFA1,"lastfal",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,N'ULL CONTROL , IDC LASTFA2,"lastfa2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,N'ULL CONTROL , IDC LASTFA3,"lastfa3",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC LASTFB1,"lastfbl",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC LASTFB2,"lastfb2",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,N'ULL CONTROL , IDC LASTFB3,"lastfb3",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL , IDC UPDATESTATUS,"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}, NANINT,"NANINT",NANFLT,NANFLT,NULL LEVEL,NULL ACCESS,NULL CONTROL}, /* pid2param: returns the parameter struct given the parameter id */
M4SPARAM *pid2param(M4SPARAMID pid) int i = 0 ;
for (i = 0; i < sizeof(m4sparameters)/sizeof(m4sparameters[O]); i++) if (m4sparameters [i] .pid == pid) return(&m4sparameters [i] ) ;
M BUGALERT("Bad parameter id.");
return(&m4sparameters[i-1]);
/* pid2name: returns name given the pid */
char *pid2name(M4SPARAMID pid) return(pid2param(pid)->name);
%* pid2low: return low field*/
double pid2low(M4SPARAMID pid) return(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- */
/* Note: 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 anymore) So we replace e0*, e-0*, and a+0* with a*, e-*, and a+* */
char *compressexponent(char *numstring) t char *e = strpbrk(numstring, "eE");
if (NULL =- e) /* no a or E in the string--leave as is */
else if ('0' -- a[1] && a[2] !_ '\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 */

ca2~ 56sso /* .(5/28/94) Phil Gabler reported that he noted that windows became ~snusable (VERY SLOW) when MACC4SSP was run on a system that had Sw~rtscan Plus but no SmartCard. He also reported that E,'.OR 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 SCIL 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 =- ERROR SMARTCARD_TIMEOUT) *err = ERROR SMARTCARD_TIMEOUT;
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) simSCReadAnalogPoint(m4spointname(eid,pid), &f, &err);
else SCReadAnalogPoint(m4spointname(eid,pid), &f, &err);
}aybestoreerror(eid, pid, err);
i f ( a rr =- ERROR NONE ) return(decodedouble(encodefloat(f)));
/* the curious encode/decode above is a hack to simplify numeric formatting*/
else return (NANFLT);
/*scisetdouble: sends value to the SSP point associated with engine,parameter*/
SCIError scisetdouble(M4SENGINEID eid, M4SPAR.AMID pid, double x) {
float f = x;
SCIError err = ERROR NONE;
if (!timedoutbefore(eid, &err)) {
if (m4e[eidl.simulatessp) simSCWriteAnalogPoint(m4spointname(eid,pid), (const float) f, &err);
else SCWriteAnalogPoint(m4spointname(eid,pid), (const float) f, &err);
maybestoreerror(eid, pid, err);
return(err);
/* scigetbit: retrieves flag associated with an engine,parameter from SSP */
BOOLPLUS scigetbit(M4SENGINEID eid, M4SPARAMID pid) {
BOOL bit;
SCIError err = ERROR NONE;

if (m4e [eid] . local input ) C A 215 6 8 8 0 ' bit = ini2int (eid, pid) ;
e1' if (!timedoutbefore(eid, &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 FOU1~D:
return("ERROR PROJECT NOT FOUND");
case ERROR SMARTSCAN PLUS NOT RUNNING:
return("ERROR SMARTSCAN NOT_RUNNING");
case ERROR SMARTCARD TIMEOUT:
return("ERROR SMARTCARD TIMEOUT").;
case ERROR POINT NAME NOT FOUND: /* SCIL errors that involve points*/
sprintf(scierr, "ERROR %s NOTFOUND", m4spointname(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 happen (or else it's a bug): */
case ERROR LINK ALREADY ESTABLISHED:
return("BUG ALREADY LINKED");
case ERROR LINK NOT CURRENTLY_ESTABLISHED:
return("BUG NOT LINKED");
default:
return("BUG UNRECOGNIZED ERROR");

' /* Title: returns the title bar for the dialog box (acts as status line)*/
char-title (M4SENGINEID eid) {
static char titlebuf[MAXSTRING+1];
sprintf(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);/*can't read all action flags;wait til next timer*/
else if (update + reinit + undo =- 0) /* either no, ...*/
return(NOOPSSPREQUEST);
else if (update + reinit + undo > 1) { /* or several, actions requested */
M M4SERROR("Ambiguous request was ignored.");
return(AMBIGUOUSSSPREQUEST);
else.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 - scigetdouble(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(OUTOFRANGESSPREQUEST);
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 &&

!inrange(nh3, pid2low(IDC
NH3),pid2high(IDC
NH3))) . ~ M M4SERROR2(rangeerror(IDC

NH3));

return(OUTOFR.ANGESSPREQUEST) else { /* all */
inputs in allowed range so...

m4e[eid].blr.ph st - ph; /* store */
data needed to service reque 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[MAXSTRING+1];

flagtext[0]
- (m4e[eid].blr.updatestatus&E

INDETERMINATE) ? '1': " ;

flagtext[1] ;
- (m4e[eid].blr.updatestatus&E

HOX UNREACHABLE) ? '2' ' ' flagtext[2] ';
- (m4e[eid].blr.updatestatus&E

OUTOFBOX TOOLONG) ? '3'~' _ ' flagtext [3) ;
- (m4e [eid]
.blr.updatesta~tus&E
POINT UNREACHABLE)?
'4' ~' flagtext [4] "
_ (m4e [eid] ;
.blr.updatestatus&E
BOX UNMAINTAINABLE) ? ' S' :

flagtext[5]= ';
(m4e[eid].blr.updatestatus&E

POINT UNMAINTAINABLE) ?'6'~' -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':' ';

flagtext[8]= ';
(m4e[eid].blr.updatestatus & E INCONSISTENT
P04) ? '9'~' flagtext[9] _ - (m4e[eid].blr.updatestatus ';
& E INCONSISTENT
PH) ? 'A':' flagtext[10] - (m4e[eid] .blr.initstatus!=E INITOK) ? 'B'-' ';

flagtext[11] - m4e[eid].badchecksum ? 'C'-' ';

flagtext [12]
_' \0' ;

return(flagtext);

/* logfile: returns the name of the MACC4SSP log file for a given engine */
char *logfile(M4SENGINEID eid) {
} return(lstrcat(lstrcat(m4spath(),nameprefix(eid)),".LOG"));
/* fileexists: checks if a file exists. */
BOOL fileexists(char *fname) {
unsigned attr=0;
return(( dos-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" ) ; C ~ 215 b 8 8 0 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[6*MAXSTRING+1];
if (lastbyte > 1) { /* file not empty, assume it already has header*/
lstrcpy(tobeprinted, "");
}lse { /* empty file, add header to it */
sprintf(tobeprinted, "%-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");
/* app?nd 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 (0] , m4e [eid] .blr. fb [1] , m4e [eid] .blr. fb (2] , m4e (eid] .blr.dt [0] , m4e [eid] .blr.dt [1] ) ;
.if ( lwrite(hFile, tobeprinted, lstrlen(tobeprinted)) <= 0) M_M4SERROR("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 */
BOOL maybeclearactions(M4SENGINEID eid) {
if ( ! m4e [eid] . actionsuptodate) {
if (m4e [eid] . localinput) {
int2ini(eid, IDC UPDATE, FALSE);
int2ini(eid, IDC UNDO, FALSE);
int2ini(eid, IDC REINIT, FALSE);
m4e[eid].actionsuptodate = TRUE;
else {
m4eLeid].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,fS,f6,f7,f8,f9,f10,fl1,f12;
#define M BIT(eid,flag) ((m4e(eid].blr.updatestatus & (flag))!=0) ~~~m~~~u if (!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 BIT(eid,E BOX UNREACHABLE))==ERROR NONE);
f3=(scisetbit(eid,IDC FLAG3, M BIT(eid,E OUTOFBOX 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 FLAG6, M_BIT(eid,E POINT UNMAINTAINABLE)) -=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 FLAGS, M BIT(eid,E NOTUPDATED)f==ERROR NONE);
f9=(scisetbit(eid,IDC FLAG9, M BIT(eid,E INCONSISTENT P04))==ERROR NONE);
f10=(scisetbit(eid,IDC FLAG10,M BIT(eid,E INCONSISTENT PH))==ERROR_NONE);
fll=(scisetbit(eid,IDC_FLAG11,m4e[eid].blr.initstatus!=E_INITOK) -=ERROR NONE);
f12=(scisethit(eid,IDC FLAG12,m4e[eid].badchecksum)==ERROR NONE);
m4e [eid] . flagsuptodate = (fl && f2 && f3 && f4 && f5 && f6-&& f7 && f8 &&
f9 && f10 && fll && f12);
}eturn(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 ? "*" . "") ;
s2ctrl(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].localinput) {
s2ctrl(GetDlgltem(hDlg, IDC PH), encodeshortf~loat(m4e[eid].blr.ph));
s2ctrl(GetDlgItem(hDlg, IDC=P04), encodeshortfloat(m4e[eid].blr.po4));
if (m4e [eid] . nh3enabled) s2ctrl(GetDlgItem(hDlg, IDC NH3), encodeshortfloat(m4e[eid].blr.nh3));
s2ctrl(GetDlgItem(hDlg, IDC BDTEMP), encodeshortfloat(m4e[eid]-blr.bdtemp));
}2ctr1(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.l,m4e[eid].fwflow)));

s2ctrl (GetDlgItem (hDlg, IDC MINFWNA) , C Q 215 6 8 8 0 . encodefloat(naleak2fwna(m4e[eid].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(GetDlgltem(hDlg, IDC DPH), encodefloat(m4e[eid].blr.dph));
/* note: macc4.c's a init() may fill in min/max bd and naleak, dpo4 and dph, hence above Tines to update screen to reflect such fill-ins */
s2ctrl(GetDlgItem(hDlg,IDC DATETIME), m4sdatetime(m4e[eid].blr.lastt));
s2ctrl(GetDlgItem(hDlg,IDC AFEED), encodefloat ( (float) a afeed (&m4e [eid] .blr) ) ) ;
s2ctrl(GetDlgItem(hDlg,IDC BFEED), encodefloat ( (float) e-bfeed(&m4e [eid] .blr) ) ) ;
s2ctrl(GetDlgItem(hDlg,IDC_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 */
d{uble 'deltat /* 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 - e-phmodel(&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) . "") sprintfZboilerlist, "%s%s%s%s%s%s%s%s%s%s", M HOILERTEXT(0), M HOILERTEXT(1) M BOILERTEXT(2), M BOILERTEXT(3), M BOILERTEXT(4), M BOILERTEXT(5), M BOILERTEXT(6), M BOILERTEXT(7), M BOILERTEXT(S), 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. Unless MACC4SSP is "
"re-launched promptly, this could result in poor chemical control."
' 'boilerlist+2, (numberrunning > 1) ? "are" : "is", 'wmberrunning > 1) ? "these boilers" . "this boiler");
return ( closewarningtext ) ; C A 215 6 8 8 0 /* setfocusnextboiler: sets focus to the next boiler's selection button*/
/* (assumes boiler selection button id's are sequential) */
void setfocusnextboilerbutton(HWND hDlg, M4SENGINEID eid) {
SetFocus(GetDlgItem(hDlg,IDC ENGINEO+(eid+1)%(MAXENGINEID+1)));
_ /* WndProc: handles all messages Windows sends to our main window/dialog box */
{ong 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 */
int ctrlmsg = HIWORD(lParam);
int eid = 0;
switch(message) {
case WM CREATE:
/*
Note 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 sciinit~)==ERROR NONE.
*/ -for (numberrunning = 0, eid = MINENGINEID; eid <= MAXENGINEID; eid++){
m4e[eid].running=fixnanint(ini2int(eid,IDC RUNNING), FALSE);
if (m4e [eid] . running) {
numberrunning++;
if (ini2engine(eid)) { /* bad checksum -- alert them */
' M M4SERROR( "BAD CHECKSUM!-Cannot restore state of a running engine!"); _ ~ogaction(eid, IDC LAUNCH);
m4e[eid].currentevent = nullevent;
updatessppoints(eid);/*brings SSP points to well defined state*/
/*.cur}ent 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;
f case WM_~0!~!AND : C Q 215 6 8 8 0 switch (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]-simulatessp);
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*/
constraininputs(hDlg, currentengineid, windowlocked);
s2ctrl(GetDlgItem(hDlg, IDC POINTNAMES),pointnames(currentengineid));
break;
case IDC UPDATE:
case IDC UNDO:
case IDC REINIT:
M ASSERT(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(GetDlgItem(hDlg, 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; eQ2156880 case IDC FORECAST:
M ASSERT(m4e[currentengineid].running);
predictphpo4(currentengineid, max(O.O,fixnandbl(ini2dbl(currentengineid, IDC_FORECASTTIME),0.0)));
break;
/* Numeric 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 to hold initially (ini2dlg() call in IDC LOADFORM
command 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 (these 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 to 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 remembered 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 HDTEMP: case IDC DPHSETPOINT:
case IDC PHSETPOINT: case IDC DPH: case IDC PH: case IDC_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 IDC FBMAX: case IDC FBMIN: case IDC_RATIOB:
case IDC RATIOA: case IDC P04A: case IDC FADEF:
case IDC FAMAX: case IDC FAMIN: case IDC HD:
case IDC FORECASTTIME: case IDC FWFLOW: case IDC NH3:
switch(ctrlmsg) {
static HWND currentctrl = NULL; /* handle to control in focus*/
static BOOL ctrlchanged = 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;

2 ~ 5~$g~
currentctrl = hCtrl;
lstrcpy(lasttext,ctrl2s(hCtrl)); /* assumes what is in there*/
break; /* is a valid number */
case EN CHANGE: /* contents of edit control changed*/
if (hCtrl != currentctrl) { /* if not{in focus, force numeric*/
if (!isvalidnumber(ctrl2s(hCtrl))) s2ctrl(hCtrl, NANSTRING);
s2ini(currentengineid, 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, 0);
/*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 { /* valid new input: remember that control changed*/
ctrlchanged = TRUE;
lstrcpy(lasttext,ctrl2s(hCtrl)); /* store validated input*/
break;
case EN KILLFOCUS: /* exiting the edit control*/
if (ctrlchanged) {
if (!isvalidnumber(ctrl2s(hCtrl))) s2ctrl(hCtrl, "");
M M4SERROR("Incomplete numeric entry cleared.");
else 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 <= MAXENGINEID; eid++) setbuttontext (hDlg, eid) ; C Q 215 6 8 8 0 ' 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 ENGINES: case IDC ENGINE6:
case IDC ENGINE7: case IDC ENGINE8: case IDC ENGINES:
/* next Line 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, currentengineid);
SetWindowText(hDlg, title(currentengineid));
constraininputs(hDlg, currentengineid, windowlocked);
/* allow for quickly reviewing each configuration in turn via the space bar:*/
setfocusnextboilerbutton(hDlg, currentengineid);
break;
case IDC START:
M ASSERT(!m4e[currentengineid].running);
m4e[currentengineid].blr = *e undefined boiler();
ini2engine(currentengineid); -m4e(currentengineid].starttime = timeinhours();
m4e[currentengineid].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(GetDlgItem(hDlg, IDC FWFLOW));
else if ( m4e[currentengineid].hh3enabled &&
NANFLT =- m4e[currentengineid].blr.nh3 ) {
M M4SERROR("Ammonia must either be specified-or disabled.");
SetFocus(GetDlgItem(hDlg, IDC NH3));
else if (E INITOK != a init( &m4e(currentengineid].blr ) ) {
M M4SERR25R (initerr [m4e (current engine id] .blr. initstatus] .msg) ;
SetFocus(GetDlgItem(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));
updatessppoints(currentengineid);
m4e[currentengineid].scierr = ERROR NONE;
m4e[currentengineid].currentevent =-nullevent;
engine2ini(currentengineid);
logaction(currentengineid, IDC START);
SetWindowText(hDlg, title(currentengineid));
setbuttontext(hDlg, currentengineid);
clearmarque();
M M4SINF0("");
numberrunning++;
break;
case IDC STOP:
M ASSERT(m4e(currentengineid].running);
i~ (IDOK== MessageBox(hDlg, oktostopmessage(currentengineid), "MACC4SSP Confirmation", Z.I S108~0 GA2156~~U
MB OKCANCEL~MB-ICONQUESTION)) {
m4e[currentengineid].running = FALSE;
constraininputs(hDlg, currentengineid, windowlocked);
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].badchecksum) /*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[eid]:localinput) simulateoperator(eid); /* simulate operator updates, etc. */
switch (getssprequest(eid)) {
HCURSOR holdcursor;
case NOOPSSPREQUEST: /* no action requested */
break;
case UNDOSSPREQUEST:
holdcursor = SetCursor(LoadCursor(NULL,IDC WAIT));
if (E INITOK != m4e[eid].blr.initstatus m4stimeinhours(eid) > m4e[eid].blr.laltt+UNDOINTERVAL
! a undo (&m4e [eid] .blr) ) M M4SERROR("Could not UNDO");
updatessppoints(eid);
engine2ini(eid); .
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 UPDATEOK) M M4SERROR("Update warning--check flags.");
updatessppoints(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(eid, IDC REINIT);
SetCursor(holdcursor);
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;
if (E INITOK =- m4e[eid].blr.initstatus) {
scisetdouble(eid,IDC AFEED,e afeed(&m4e(eid].blr));/*set 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, a congruencyratio(&m4e[eid].blr));
}aybeclearactions(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(curreritengineid));
}eturn(0);
case WM CLOSE:
if (windowlocked) { /* cannot close locked window */
M M4SERROR("You must enter the password before closing MACC4SSP");
return(0);
}
else if (numberrunning > 0 &&
IDOK != MessageBox(hDlg, closewarningmessage(numberrunning), "MACC4SSP Warning", MB OKCANCEL~MB_ICONHAND)) return(0); /* short circuit close */
else { /* log the user's CLOSE action for each running boiler */
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 ) ; C A 215 6 8 8 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 Press) */
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) MSG msg;
WNDCLASS wndclass;
HCURSOR 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 = (W'NDPROC) WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = DLGWINDOWEXTRA;
wndclass.hInstance.= hInstance;
wndclass.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(MACC4SSP ICON));
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));
hmainwin=CreateDialog(hInstance, MAKEINTRESOURCE(MACC4SSP DIALOG), 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 timer");
return(0);
}lse ShowWindow(hmainwin, nCmdShow);
SendMessage(hmainwin, WM CON~IAND, IDC LOADFORM, 0); /* loads dlg box */
/* cannot do this in WM CREATE because Windows creates dialog box controls*/

/* AFTER it sends WM_CREATE; plays the role WM INITDIALOG usually would */
/*.,if we were using an ordinary DlgProc (we are using a WndProc instead)*/
while (GetMessage(&msg, NULL, 0, 0)) if (!IsDialogMessage(hmainwin, &msg)) TranslateMessage ( &msg) ; C A 215 6 8 8 0 DispatchMessage(&msg);
return (msg.wParam);

' chaff C A 215 6 8 8 0 *encodenumber (double x, . const char *fmt) {

' Static char encodeddouble[MAXSTRING+1];

~' (x =-NANFLT) .st cpy(encodeddouble, NANSTRING);

e'Ts a {

spri ntf(encodeddouble, fmt, x);

compressexponent(encodeddouble);

}eturn(encodeddouble);

/*encodedouble: character string convert.a */
double-with full precision--to a char *encodedouble(double x) {

return(encodenumber(x,DOUBLEPRECISIONFORMAT));

/* decodedouble:
convert a character string to a double */

double decodedouble(char *s)~

return );
( (lstrcmp(s,NANSTRING) -- 0) ? NANFLT
. atof(s) /*encodefloat: character string convert */
a double to a full float precision char *encodefloat(double x) {

return(encodenumber(x,FLOATPRECISIONFORMAT));

/*encodeshortfloat: precision string*/
convert double to less than full float char *encodeshortfloat(double x) return(encodenumber(x,SHORTFLOATPRECISIONFORMAT));

/*encodeint:
convert an integer to a character string */

char *encodeint(int i) t static char encodedin [MAXSTRING+1];

if (NANINT
=- i) lstrcpy(encodedint,NANSTRING);

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];

*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));

return(rangeerrortext);

/* timeinhours: returns time in hours since Jan 1, 1970 */
dou . timeinhours(void) {
return ( ((double) time(NULL))/SECONDSPERHOUR );
/* m4stimeinhours: returns the time seen (simulated or real) by engine */
double m4stimeinhours(M4SENGINEID eid) {
return ( ! m4e [eid] . simulatessp ?
timeinhours ( ) . (m4e [eid] . starttime +
(SIMHOURSPERREALSECOND*SECONDSPERHOUR*(timeinhours()-m4e[eid].starttime))));
/* m4sdatetime: given hours since Jan 1, 1970, returns datetime string */
char *m4sdatetime(double m4stime) static char datetime[MAXSTRING+1]{;
time t t = (time t) (m4stime * SECONDSPERHOUR); /* convert to C time format*/
strftime(datetime,MAXSTRING,"%d-%b-%Y %X", localtime(&t));
return(datetime);
/* begin: code to perform conversions from macc4.c module's internal moles/hr sodium leak units to the feedwater (lbs/hr) plus feedwater Na (ppb) values that macc4ssp presents to the user */
#define MW NA 22.99 /* molecular weight of Na */
double lb2kg(double 1b) return((NANFLT =- 1b) ? NANFLT . (lb*0.45359));
double kg2lb(double kg) ~ return((NANFLT =- kg) ? NANFLT . (kg/0.45359));
double ppb2molesperkg(double ppb, double mw) {
return ((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 multiplication */
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 ~~ x2==NANFLT f) x2==0.0) ? NANFLT . (xl/x2));
%* naleak2fwna: converts the Na leak units (moles/hr) used by macc4.c to feedwater Na (ppb) used by macc4ssp.c's user interface.
feed water flow in lbs/hr.*/
double naleak2fwna(double naleak, double fwflow) return(molesperkg2ppb(nandiv(naleak,lb2kg(fwflow)), MW NA));
/* fwna2naleak: inverse of naleak2fwna (see above) */
double fwna2naleak(double fwna, double fwflow) return(nanmult(ppb2molesperkg(fwna, MW_NA), lb2kg(fwflow)));
/*end: code to perform unit conversions */
/*fixnanint: defaulting for undefined ints */

int f ixnanint ( int i, int fornan) { C A 215 6 8 8 0 return (i==NANINT ? fornan . i);
doubt: fixnandbl(double x, double fornan) {
return (x==NANFLT ? fornan . x);
/* m4spath: MACC4SSP path is the 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 =
GetModuleFileName(GetModuleHandle(M4SMODULENAME), path, 2*MAXSTRING);
for (len--; len >= 0 && path[len]!='\\'; len--) /* zap name.ext part*/
path [len] _' \0' ;
return (path) ;
/* ctrl2s: returns the string associated with a control (or window) */
char *ctrl2s(HWND hCtrl) static char s[MAXSTRING{1];
GetWindowText(hCtrl, s, MAXSTRING);
return(s);
/* s2ctrl: copies text to the given control */
void s2ctrl(HWND hCtrl, char *s) SetWindowText(hCtrl, s);
static char marquebuf[2*MAXSTRING+1]; /* stores scrolling marque */
/* clearmarque: clears the marque */
void clearmarque(void) {
lstrcpy(marquebuf,"");
/* marque: uses message line as a scrolling marque */
void marque(char *s) {
static BOOL firsttime = TRUE;
i.f (firsttime) clearmarque();
firsttime = FALSE;
lstrcat(marquebuf, s); /* add new string to end of old one */
if (lstrlen(marquebuf) > MAXSTRING) /*destroy older part of marque*/
lstrcpy(marquebuf, marquebuf+lstrlen(marquebuf)-MAXSTRING);
s2ctrl(GetDlgItem(hmainwin, IDC MESSAGELINE),marquebuf);
UpdateWindow(GetDlgItem(hmainwin, IDC MESSAGELINE));
/* nameprefix: returns the macc4 prefix associated with a given engineid */
char *nameprefix(M4SENGINEID eid) {
static char prefix[MAXSTRING+1];
sprintf(prefix, "M%i", eid);
return(prefix);

~ /* inifile: returns the name of the engine's initialization file */
Chad tinifile (M4SENGINEID eid) {
return(lstrcat(lstrcat(m4spath(),nameprefix(eid)),".INI°));
/*m4spointname: SSP point name for engineid/paramid pair */
char *m4spointname(M4SENGINEID eid, M4SPAR.AMID pid) static char pointname[MAXSTRING+1];
lstrcpy(pointname, nameprefix(eid));
} return(lstrcat(pointname,pid2name(pid)));
/* updatechecksum: simple hash 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];
GetPrivateProfileString( M4SMODULENAME, pid2name(pid), NANSTRING, s, MAXSTRING, inifile(eid));
updatechecksum(eid, pid, s);
return(s);
/* s2ini: writes a string to an initialization file */
void s2ini(M4SENGINEID eid, M4SPAR.AMID pid, char *s) if (!WritePrivateProfileString(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 fi~.e keyword */
void ctrl2ini(M4SENGINEID eid, M4SPARAMID pid, HWND hCtrl) {
s2ini(eid, pid, ctrl2s(hCtrl));
/* ini2ctrl: reads associated ini file keyword into a control */
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 keyword */

{ CA215b88C
void int2ini(M4SENGINEID eid, M4SPARAMID pid, int i) ~2ini(eid, pid, encodeint(i));
/* ini2int: reads associated ini file keyword into a memory */
int ini2int(M4SENGINEID eid, M4SPARAMID pid) {
return(decodeint(ini2s(eid,pid)));
/*pointnames: returns name of points string */
char *pointnames(M4SENGINEID 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(pointnamebuf, "%sm%i + (afeed,bfeed,ph,po4,%sbdtemp,%supdate,undo,reinit,flagl,...,flagl2)", pointsprefix, eid, m4e[eid].nh3enabled ? "nh3," "", m4e[eid].localinput ? "° . "bd,fwna,ratio,");
}eturn(pointnamebuf);
*getcheckboxstate: TRUE if checkbox checked, else FLASE */
BOOL ge~tcheckboxstate(HWND hcheckboxctrl) {
return(SendMessage(hcheckboxctrl, BM_GETCHECK, 0, 0) ? TRUE . FALSE);
%* setcheckboxstate: checks or unchecks a checkbox */
void setcheckboxstate(HWND hcheckboxctrl, BOOL checked) {
SendMessage(hcheckboxctrl, BM SETCHECK, checked, 0);
~* ini2dlg: loads all fields from the ini file into dialog box (main window) */
checkboxes, which indicated controller modes, are loaded into memory as well as into the form--this makes modes easily available.(without disk access) later on.
*/
void ini2dlg(HWND hDlg, int eid) {
M4SPARAM *currentctrl = pid2param(IDC FIRSTCONTROL);
M4SPARAM *lastctrl - pid2param(IDC LASTCONTROL);
#define M INI2CHECKBOX(wheretostore,pid,default) \
wheretostore = fixnanint(ini2int(eid,pid),default), \
setcheckboxstate(GetDlgItem(hDlg, 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 ( ; currentctrl <= lastctrl; currentctrl++) /*load form's edit controls*/
if (STOREDEDIT CONTROL =- currentctrl->type) /* with this engine's info */
ini2ctrl(GetDlgItem(hDlg, currentctrl->pid), eid, currentctrl->pid);

/* .
' constraininputs: constrains inputs to the control based on both r'~ kinds of users who are allowed to access it as well as the ~,,~.ditions 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) M4SPARAM *currentctrl = pid2param(IDC FIRSTCONTROL);
M4SPARAM *lastctrl - pid2param(IDC LASTCONTROL);
HOOL localinput - m4e[eid].locaIinput;
BOOL running m4e [eid] . running;
BOOL nh3enabled - m4e[eid].nh3enabled;
BOOL userisamanager - !windowlocked;
for currentctrl c= {
( ; lastctrl; currentctrl++) BOOL manageronlyctrl - MANAGERONLY LEVEL& currentctrl->level;

BOOL operatoronlyctrl = OPERATORONLY LEVEL& currentctrl->level;

BOOL staticctrl STATIC ACCESS & currentctrl->access;

BOOL dynamicctrl - DYNAMIC ACCESS & currentctrl->access;

BOOL localctrl - LOCAL ACCESS & currentctrl->access;

BOOL nh3ctrl - NH3 ACCESS & currentctrl->access;

BOOL enablectrl = TRUE; /* enabled until proven otherwise */
/* Decide if control is enabled by a process of elimination: consider, in turn, each condition which could require that the control be disabled*/
if (manageronlyctrl && !userisamanager) enablectrl = FALSE;
else if (operatoronlyctrl && userisamanager) /* password prompt */
enablectrl = FALSE; /*(managers have already entered the password)*/
else if (nh3ctrl && !nh3enabled) enablectrl = FALSE;
else if (staticctrl && running) enablectrl = FALSE;
else if (dynamicctrl && !running) enablectrl = FALSE;
else if (localctrl && running && !localinput) enablectrl = FALSE;
else if (localctrl && !running && !userisamanager) enablectrl = FALSE;
/* else, since there are no objections from the authorities, control enabled*/
EnableWindow(GetDlgItem(hDlg, currentctrl->pid), enablectrl);
} }
/* ini2engine: loads all parameters from ini file into memory */
/* returns badchecksum: TRUE implies a bad check sum/corrupted file*/
BOOL ini2engine(M4SENGINEID eid) {
int oldchecksum = ini2int(eid, IDC CHECKSUM);
m4e[eid].blr.po4 = ini2dbl( eid, IDC P04 );
m4e [eid] .blr.ph = ini2dbl ( eid, IDC PH ) ;
m4e[eid].blr.bdtemp = ini2dbl( eid,-IDC BDTEMP );
m4e[eid].blr.nh3 = ini2dbl( eid, IDC NH3 );
m4e[eid].lasteventtime - ini2int ( eid, IDC LASTEVENTTIME);
m4e[eid].eventid - ini2int ( eid, IDC EVENTID);
m4e[eid].checksum = 0; /* we don't include first few params in checksum */
/* cause they can legitimately change */

Ca2156880 m4e[eid].starttime = ini2dbl ( eid, IDC STARTTIME);
m4e[eid].running - fixnanint(ini2int ( eid, IDC RUNNING), FALSE);
m' ~eid].logtofile = fixnanint(ini2int ( eid, IDC LOGTOFILE), TRUE) m,., ieid] . blr . t = ini2dbl ( 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.fbmax = ini2dbl( eid, IDC FBMAX );
m4e[eid].blr.fbdef = ini2dbl( eid, IDC FBDEF );
m4e[eid].blr.po4b = ini2dbl( eid, IDC P04B );
m4e[eid].blr.ratiob = ini2dbl( eid, IDC RATIOB );
m4e[eid].blr.spgb = ini2dbl( eid, IDC SPGB );
m4e[eid].blr.m = ini2dbl( eid, IDC M T;
m4e[eid].blr.dpo4 = ini2dbl( eid, IDC DP04 );
m4e[eid].blr.po4setpoint = ini2dbl( 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 = ini2dbl( eid, IDC PHSETPOINT );
m4e[eid].blr.dphsetpoint = ini2dbl( eid, IDC D~HSETPOINT );
m4e[eid].blr.napo4ratio = ini2dbl( eid, IDC NAP04RATI0 );
m4e[eid].blr.minnapo4ratio = ini2dbl( eid, IDC MINNAP04RATI0 );
m4e[eid].blr.maxnapo4ratio = ini2dbl( eid, IDC MAXNAP04RATI0 );
m4e[eid].blr.max sample interval = ini2dbl( ei3, IDC_MAX-SAMPLE-INTERVAL );
m4e[eid].blr.max outofbox adjustment =
ini2dbl( eid, IDC MAX OUTOFBOX ADJUSTMENT );
m4e[eid].blr.max relative error = ini2dbl( eid, IDC MAX_RELATIVE-ERROR );
m4e[eid].blr.beps = ini2dbl( eid, IDC BEPS );
m4e [eid] .blr.bdmin = ini2dbl ( eid, IDC BDMIN ) ;
m4e [eid] .blr.bdmax = ini2dbl ( eid, IDC BDMAX ) ;
m4e [eid] . fwflow = ini2dbl ( eid, IDC FWFLOW) ;
m4e[eid].blr.naleakmin = fwna2naleak(ini2dbl( eid, IDC MINFWNA ), m4e [eid] . fwf low) ;
m4e[eid].blr.naleakmax = fwna2naleak(ini2dbl( eid, IDC MAXFWNA ), m4e [eid] . fwflow) ;-m4e[eid].blr.l = fwna2naleak(ini2dbl( eid, IDC FWNA), m4e [eid] .~wflow) ;
m4e[eid].blr.fdt = ini2dbl( eid, IDC FDT );
m4e[eid].blr.lastt = ini2dbl( eid, IDC LASTT );
m4e[eid].blr.lastbdtemp = ini2dbl( eid, IDC LASTBDTEMP );
m4e[eid].blr.lastpo4 = ini2dbl( eid, IDC~LASTP04 );
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 = ini2dbl( eid, IDC B4LASTP04 );
m4e[eid].blr.b4lastnh3 = ini2dbl( eid, IDC 84LASTNH3 );
m4e[eid].blr.b4lastph = ini2dbl( eid, IDC B4LASTPH );
m4e [eid] . blr . bd = ini2dbl ( eid, IDC BD ) ;
m4e[eid].blr.lastl = ini2dbl( eid, IDC LASTL );
m4e[eid].blr.lastbd = ini2dbl( eid, IDC LASTBD );
m4e [eid] .blr. dt [0] - ini2dbl ( eid, IDC DT1 ) ;
m4e [eid] .blr.dt [1] - ini2dbl ( eid, IDC DT2 ) ;
m4e [eid] .blr. fb [0] - ini2dbl ( eid, IDC FB1 ) ;
m4e [eid] .blr. fb [1] - ini2dbl ( eid, IDC FB2 ) ;
m4e [eid] .blr. fb [2] - ini2dbl ( eid, IDC FB3 ) ;
m4e [eid] .blr. fa [0] - ini2dbl ( eid, IDC FAl ) ;
m4e [eid] .blr. fa [1] - ini2dbl ( eid, IDC FA2 ) ;
m4e [eid] .blr. fa [2] = ini2dbl ( eid, IDC_FA3 ) ;
m4e [eid] .blr.lastdt [0] - ini2dbl ( eid, IDC LASTDT1 ) ;
m4e[eid].blr.lastdt[1] - ini2dbl( eid, IDC LASTDT2 );

m4e (eid] .blr.lastfa [0] - ini2dbl ( eid, IDC LASTFA1 ) ;
m~4e [eid] .blr, lastfa [1] - ini2dbl ( eid, IDC-LASTFA2 ) ;
mS.e [eid] .blr. lastfa [2] - ini2dbl ( eid, IDC LASTFA3 ) ;
m4~ [eidl . blr. lastfb [0] - ini2dbl ( eid, IDC LASTFB1 ) % ~ A ~ ~ ~ ~ 8 m ~eid] .blr.lastfb [1] - ini2dbl ( eid, IDC LASTFB2 ) ;
m~[eid] .blr. lastfb [2] - ini2dbl ( eid, IDC_LASTFB3 ) ;
m4e [eid] . blr . updatestatus =
fixnanint(ini2int( eid,IDC UPDATESTATUS),E UPDATEOK);
m4e(eid].blr.lastupdatestatus-=
fixnanint(ini2int( eid, IDC LASTUPDATESTATUS ), E UPDATEOK);
m4e[eid].blr.initstatus =fixnanint(ini2int( eid, IDC INITSTATUS),!E INITOK);
m4e[eid].blr.undoable = fixnanint(ini2int( eid, IDC UNDOABLE), FALSE);
m4e[eid].simulatessp = fixnanint(ini2int ( eid, IDC SIMULATESSP),FALSE);
m4e[eid].localinput = fixnanint(ini2int ( eid, IDC LOCALINPUT),FALSE);
m4e(eid].nh3enabled = fixnanint(ini2int ( eid, IDC NH3ENABLED),FALSE);
return (m4e [eid] . badchecksum =
(m4e[eid].running && /* ignore if engine not running or */
oldchecksum != IGNORECHECKSUM && /* if ignore checksum is flagged */
oldchecksum != m4e(eid].checksum));
/* engine2ini: writes in memory engine representation to INI file */
void engine2ini(M4SENGINEID eid) dbl2ini( eid, IDC P04, m4e(eid].blr.po4 );
dbl2ini ( eid, IDC PH, m4e (eid] .blr.ph ) ;
dbl2ini( eid, IDC BDTEMP, m4e(eid].blr.bdtemp );
dbl2ini( eid, IDC NH3, m4e[eid].blr.nh3 );
int2ini ( eid, IDC LASTEVENTTIME, m4e[eid].lasteventtime );
int2ini ( eid, IDC EVENTID, m4e (eid] . eventid ) ;
m4e[eid].checksum = 0; /* don't include~above params in checksum*
/* cause they can legitimately change */
dbl2ini ( eid, IDC STARTTIME, m4e[eid].starttime);
int2ini ( eid, IDC RUNNING, m4e[eid].running );
int2ini ( eid, IDC LOGTOFILE, m4e[eid].logtofile );
dbl2ini( eid, IDC T, m4e[eid].blr.t );
dbl2ini( eid, IDC FAMIN, m4e(eid].blr.famin );
dbl2ini( eid, IDC FAMAX, m4e(eid].blr.famax );
dbl2ini( eid, IDC FADEF, m4e(eid].blr.fadef );
dbl2ini( eid, IDC P04A, m4e(eid].blr.po4a );
dbl2ini( eid, IDC RATIOA, m4e(eid].blr.ratioa );
dbl2ini( eid, IDC SPGA, m4e[eid].blr.spga );
dbl2ini( eid, IDC FBMIN, m4e[eid].blr.fbmin );
dbl2ini( eid, IDC FBMAX, m4e[eid].blr.fbmax );
dbl2ini( eid, IDC FBDEF, m4e(eid].blr.fbdef );
dbl2ini( eid, IDC P04B, m4e(eid].blr.po4b );
dbl2ini( eid, IDC RATIOB, m4e[eid].blr.ratiob );
dbl2ini( eid, IDC SPGB, m4e[eid].blr.spgb );
dbl2ini ( eid, IDC M, m4e (eid] .blr.m ) ;
dbl2ini( eid, IDC DP04, m4e(eid].blr.dpo4 );
dbl2ini( eid, IDC P04SETPOINT, m4e[eid].blr.po4setpoint );
dbl2ini( eid, IDC MINP04, m4e[eid].blr.minpo4 );
dbl2ini( eid, IDC MAXP04, m4e(eid].blr.maxpo4 );
dbl2ini( eid, IDC DPH, m4e(eid].blr.dph );
dbl2ini( eid, IDC PHSETPOINT, m4e[eid].blr.phsetpoint );
dbl2ini( eid, IDC DPHSETPOINT, m4e[eid].blr.dphsetpoint );
dbl2ini( eid, IDC NAP04RATI0, m4e(eid].blr.napo4ratio );
dbl2ini( eid, IDC MINNAP04RATI0, m4e(eid].blr.minnapo4ratio );
dbl2ini( eid, IDC MAXNAP04RATI0, m4e(eid].blr.maxnapo4ratio );
dbl2ini( eid, IDC MAX SAMPLE INTERVAL, m4e[eid].blr.max-sample_interval );
dbl2ini( eid, IDC MAX OUTOFBOX ADJUSTMENT, m4e[eid].blr.max outofbox adjustment );
dbl2ini( eid, IDC MAX RELATIVE ERROR, m4e[eid].blr.max-relative error );
db'l2ini ( eid, IDC BEPS, m4e [ei3] .blr.beps ) ;
dY- mini ( eid, IDC BDMIN, m4e [eid] .blr.bdmin ) ;
d~,,_,..ini ( eid, IDC BDMAX, m4e (eid] .blr.bdmax ) ;
dbl2ini( eid, IDC FWFLOW, m4e[eid].fwflow );
dbl2ini( eid, IDC MINFWNA, decodedouble(encodefloat( naleak2fwna (m4e [eid] .blr.naleakmin, m4e [eid] . fwflow) ) ) ) ;
dbl2ini( eid, IDC MAXFWNA, decodedouble(encodefloat( naleak2 fwna (m4e [eid] . blr. naleakmax, m4e [eid] . fwf low) ) ) ) ;
dbl2ini( eid, IDC FWNA, decodedouble (encodefloat (naleak2fwna (m4e [eid] .blr. l,m4e [eid] . fwflow) ) ) ) ;
dbl2ini( eid, IDC FDT, m4e[eid].blr.fdt );
dbl2ini( eid, IDC LASTT, m4e(eid].blr.lastt );
dbl2ini( eid, IDC LASTBDTEMP, m4e[eid].blr.lastbdtemp );
dbl2ini( eid, IDC LASTP04, m4e[eid].blr.lastpo4 );
dbl2ini( eid, IDC LASTNH3, m4e[eid].blr.lastnh3 );
dbl2ini( eid, IDC LASTPH, m4e[eid].blr.lastph );
dbl2ini( eid, IDC B4LASTT, m4e[eid].blr.b4lastt );
dbl2ini( eid, IDC B4LASTBDTEMP, m4e(eid].blr.b4lastbdtemp );
dbl2ini( eid, IDC B4LASTP04, m4e[eid].blr.b4lastpo4 );
dbl2ini( eid, IDC B4LASTNH3, m4e[eid].blr.b4lastnh3 );
dbl2ini( eid, IDC B4LASTPH, m4e[eid].blr.b4lastph );
dbl2ini( eid, IDC BD, decodedouble(encodefloat(m4e[eid].blr.bd)) );
dbl2ini( eid, IDC LASTL, m4e[eid].blr.lastl );
dbl2ini( eid, IDC LASTBD, m4e[eid].blr.lastbd );
dbl2ini ( eid, IDC DT1, m4e [eid] .blr.dt [0] ) ;
dbl2ini( eid, IDC DT2, m4e(eid].blr.dt(1] );
dbl2ini ( eid, IDC FB1, m4e [eid] .blr. fb [0] ) ;
dbl2ini ( eid, IDC FB2, m4e [eid] . blr. fb [1] ) ;
dbl2ini ( eid, IDC FB3, m4e [eid] .blr. fb [2] ) ;
dbl2ini ( eid, IDC FAl, m4e [eid] .blr. fa [0] ) ;
dbl2ini ( eid, IDC FA2, m4e [eid] .blr. fa [17 ) ;
dbl2ini ( eid, IDC FA3, m4e [eid] .blr. fa [2] ) ;
dbl2ini( eid, IDC LASTDT1, m4e[eid].blr.lastdt[O] );
dbl2ini ( eid, IDC LASTDT2, m4e [eid] .blr.lastdt (1] ) ;
dbl2ini( eid, IDC LASTFAl, m4e[eid].blr.lastfa[0] );
dbl2ini( eid, IDC LASTFA2, m4e[eid].blr.lastfa(1] );
dbl2ini( eid, IDC LASTFA3, m4e[eid].blr.lastfa[2] );
dbl2ini ( eid, IDC LASTFB1, m4e [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 UPDATESTATUS , 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, IDC LOCALINPUT, m4e[eid].localinput );
int2ini ( eid, IDC NH3ENABLED, m4e[eid].nh3enabled int2ini( eid, IDC CHECKSUM, m4e[eid].checksum);
/* randscierror: generates a random SCIL error code */
SCIError 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 NAME NOT FOUND, ERROR PROJECT NOT FOUND, ERROR SMARTSCAN PLUS NOT RUNNING, ' ~ ERROR-SMARTCARD TIMEOUT ~;
. . _ Cb215~880 i ( rand() <= RAND MAX*(1.0-scilerrorfraction)) return (ERROR NONEf;
else {
int nerrors = sizeof(scierr)/sizeof(scierr[0]);
return(scierr[rand() °s nerrors]);
} }
/*simulateoperator: simulates input from the operator at regular intervals*/
void simulateoperator(M4SENGINEID eid) {
double t = m4stimeinhours(eid);
if (t - m4e[eid].lasteventtime >= SIMHOURSPEREVENT) {
if (m4e [eid] . event id <
sizeof(simsspinputs)/sizeof(simsspinputs[0])-1) m4e [eid] .eventid++;
int2ini ( eid, IDC EVENTID, m4e [eid] .event id ) ;
} /* just repeat last event when we get to the end of the array*/
m4e [eid] . lasteventtime = t;
int2ini ( eid, IDC LASTEVENTTIME, m4e[eid].lasteventtime );
m4e [eid] . currentevent = simsspinputs [m4e [eid] . eventid] ;
%* else robo-operator is idle */
}
/* The following sim* functions emulate the corresponding SCIL routines:*/
void simSCInitCommLink(SCIError *err) {
*err = randscierror.(SCILERRORFRACTION);
}
void simSCCutCommLink(SCIError *err) *err = randscierror(SCILERRORFRACTION);
}
void simSCReadDigitalPoint(const char *s,BOOL *bit,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*/
if (lstrcmp(s, pid2name(IDC UPDATE))==0) *bit = m4e[eid].currentevent.update;
else if (lstrcmp(s, pid2name(IDC UNDO))==0) *bit = m4e[eid].currentevent.undo;
else if (lstrcmp(s, pid2name(IDC REINIT))==0) *bit = m4e[eid].currentevent.reinit;
else M BUGALERT("Invalid digital point during simulated SSP Read.");
} _ *err = randscierror(SCILERRORFRACTION);
}
void simSCReadAnalogPoint(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*/
if (lstrcmp(s, pid2name(IDC PH))==0) *value = e~hmodel ( &m4e [eid] . blr) ;
else if (lstrcmp(s, pid2name(IDC P04))==0) C A215688~
-*value = e~o4model (&m4e [eid] .blr) ;
e' ~ if (lstrcmp(s, pid2name(IDC NH3))==0) ..~; alue = m4e [eid] . blr . nh3 ;
else if (lstrcmp(s, pid2name(IDC BDTEMP))==0) *value = m4e[eid].blr.bdtemp; -else M BUGALERT("Invalid analog point during simulated SSP Read.");
*err = randscierror(SCILERRORFRACTION);
void simSCWriteDigitalPoint(const char *s,BOOL bit,SCIError *err) M4SENGINEID eid = s[1] - '0'; /* hack to pull off engine id */
/* from point name prefix */
if (eid =- currentengineid) {
char buf[MAXSTRING+1];
s += lstrlen(nameprefix~(eid)); /* skip over the engine specific prefix*/
if (lstrcmp(s, pid2name(IDC UPDATE))==0) {
m4e[eid].currentevent.update = bit;
sprintf(buf, "update=%i ", bit);
}lse if (lstrcmp(s, pid2name(IDC UNDO))==o) {
m4e[eid].currentevent.undo = bit;
sprintf(buf, "undo=%i ", bit);
}lse if (lstrcmp(s, pid2name(IDC REINIT))==0) {
m4e[eid].currentevent.reinit = bit;
sprintf(buf, "reinit=%i ", bit);
else if (lstrcmp(s, pid2name(IDC FLAG1))==0) sprintf(buf, "flags=(%i,", bitT; ' 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 FLAG7))==0) sprintf(buf, m4e[eid].localinput ? "flags=(%i," . "%i,", bit);
else if (lstrcmp(s, pid2name(IDC-FLAG8))==0) sprintf(buf, "%i,", bit);
else if (lstrcmp(s, pid2name(IDC_FLAG9))==0) sprintf(buf, "%i,", bit);
else if (lstrcmp(s, pid2name(IDC-FLAG10))==0) sprintf(buf, "%i,", bit);
else if (lstrcmp(s, pid2name(IDC-FLAG11))==0) sprintf(buf, "%i,", bit);
else if (lstrcmp(s, pid2name(IDC_FLAG12))==0) sprintf(buf, "%i) ", bit);
else sprintf(buf,"%s=%i ", s, bit);
marque (buf ) ;
*err = randscierror(SCILERRORFRACTION);

} . CA2156880 ' void simSCWriteAnalogPoint(const char *s,float f,SCIError *err) M ~NGINEID eid = s[1] - '0'; /* hack to pull off engine id */{
..,_ /* from point 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=%.5g ", ff;
else if (lstrcmp(s, pid2name(IDC BFEED))==0) spr{ntf(buf, "b=%.Sg ", f);
else #if TESTING
sprintf(buf,"%s=%.Sg ", s, f);
#else sprintf(buf,"");
#endif }arque(buf);
}err = randscierror(SCILERRORFRACTION);
/*isvalidnumber: if s is a valid number or numeric prefix, TRUE, else FALSE*/
HOOL 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 floats */
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 */
BOOL isnumericprefix(char *s) {
s += strspn(s, " "); /* skip leading whitespace 'cause strtod() does*/
if (lstrcmp(s,".")==0 lstrcmp(s,"-")==0 lstrcmp(s,"-.")==0 ~~
lstrcmp(s,"+")==0 lstrcmp(s,"+.")==0) return(TRUE); /* special cases of valid prefixes that are,not numbers*/
else return(isvalidnumber(s));
/* forcevalidnumber: truncates the string to make it a valid number. */
char *forcevalidnumber(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 != NANFLT && x < low) ~~ (high != NANFLT && x > high)) return(FALSE);
else return (TRUE) ;
/* forceinrange: forces x between low and high; ignores NANFLT */

double forceinrange (double x, double low, double high) { ~ A 215 6 8 8 0 i f ( x == NANFLT ) ' " return (x) ;
e_'' if (low != NANFLT && x <= low) ~~.aurn ( low) ;
else if (high != NANFLT && x >= high) return(high);
else return(x);
/* updatessppoints: sets flags that force MACC4SSP to update SSP
action flags (update, undo, and reinit) and status flags */
/*(actual 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].flagsuptodate = FALSE;/*clear old flags...*/
m4e[eid].actionsuptodate=FALSE;/*...and old actions*/
/*
SmartCard Interface Library (SCIL) based routines for accessing SmartScan Plus */
SCIError sciinit(M4SENGINEID eid) SCIError err = ERROR_NONE;
#if WORKAROUND
static BOOL initialized = FALSE;
if (initialized) m4e [eid] . scierr = err;
return(err);
#endif if (!m4e[eid].simulatessp) SCInitCommLink(&err);
if (ERROR SMARTSCAN PLUS_NOT RUNNING == err) updatessppoints(eid); /* when SSP does start up, points must be*/
/* in a well defined initial state */
m4e[eid].scierr = er.r; /* note that this sets the scierr field */
/* to ERROR_NONE if Init succeeded */.
#if WORKAROUND
if (err =- ERROR_NONE) initialized = TRUE;
#endif return(err);
SCIError scifini(M4SENGINEID eid) SCIError err=ERROR NONE;
#if WORKAROUND -return(err);
#endif if (!m4e[eid].simulatessp) SCCutCommLink(&err);
return(err);
/* maybestoreerror: remember error if the 1st seen since errors were cleared */
SCIError maybestoreerror(M4SENGINEID eid, M4SPARAMID pid, {CIError err) if (m4e[eid].scierr =- ERROR NONE && err != ERROR_NONE) m4e [eid] . scierr = err;
m4e [eid] .pidscierr = pid;
return (err) ;
%* timedoutbefore: returns true if the system timed since errors were cleared*/

05/0712003 12:01 FAX 613 787 3558 BLG CANADA f~ 002 ~3 05/07/2005 12:01 FAX 615 787 5558 ._~_~HLG CANADA ~ ___. C~OOa ~,, 0 ~~~~5~ ° ~~~~ ~ a I~S~ ,0~ ~ a ~ OQ ~ , s ~~ Oil 05/07/2003 12:01 FA7Y 61a 787 3558 ~, BI,G~AVA 1004 D

A

~ a ao ~1 a a~~ _. s o B ~ ~ " ~ N
e, os'J 0 N
y ~

r.

a a x ' V

p ~ $a DD

d r1i C
1J~

~ ~ a ~

..

c v r. E

U

12:02 FAg BLG
CANAD
~

'. - _ f ~005 _ :

r H
n~

r.~
O
t a I
_C

Cl.
,j O

C
V

_ Q N

d . v~
v n t > a_ v v N

N

V
o O
L

da s a V
9 t y . 9a 0 V

~.r _._~.____~~______...__..___.._~_r_ .

__ ~ ___~ _____ ._~___ __,__.

__ d ~
a 0 a .

a' _ ~

U Q

_ _ ~
O

GJ

V
O

a L

' c n a Q U

u V

S U

Q

N

O

Z

a a°m 6 ~' ~0.
CA 02156880 1995-08-24 , ,...........~. ..., _...,............,..._..

05/07/2043 12;02 FA$ 613 787 3558 V BLG CANADA ~ 006 m Wn v a a a w N
a . o o_ c 'o vi c a o U
H

v +-a a- N
~

c _.~-~ a_ ~o U
O

N

m U
' N G
L

O
U

U
v .n a~
Z
O
n_ CA 02156880 1995-08-24 .. "........ ,-..,_..~"_.,.,".. ....- .e.._ . , 05/07/21)03 12:02 FA.Y

BLG
CANADA

_~ .__.r. IQ
- _.~.___r..__ 00?

d d as . ~ ~

. a a ~ a _ _ ~

o a a a o ~ ~

a _z Oa n ~ ~~
~..s~~~ Lla ~~ ~ y aCl __ ~ ~ $ ~ ~ ' .. . ~ I ... .~ ~ _~ pp I o V

d .. . ..

oa ~ ~

o 0 x '~ ~' .s.~ C1 U

c~ p ~~ a ~
~ ~~

CICI ~ Q~ ~ m w ~ ~
DO

~

U O

v ' c U

x U
a .s1 U
U
I
d-O

Claims (74)

1. An automatic control system for controlling at least two interdependent chemicals in the fluid of a continuously stirred tank reactor system having an effluent flow, said control system comprising input means for receipt of fluid parameters and control means responsive to said input means, said control means using non-proportional control for automatically minimizing the time that said at least two interdependent chemicals in the fluid spend outside of a target region, and away from a setpoint, of said at least two interdependent chemicals in the fluid without controlling said effluent flow.
2. The control system of claim 1 wherein said continuously stirred tank reactor system is an industrial boiler having a boiler fluid and said effluent flow is a blowdown flow.
3. The control system of claim 2 wherein one of said fluid parameters comprises the pH of the boiler fluid and wherein said input means comprises means for determining the pH value of the fluid.
4. The control system of claim 3 wherein said means for determining the pH value of the fluid provides an on-line measurement of the pH value of the fluid.
5. The control system of claim 3 wherein said control means comprises a first feedstream and a second feedstream for feeding first and second fluid treatment materials, respectively, to the boiler fluid at respectively determined feed rates for achieving and maintaining said target region, 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.
6. The control system of claim 5 wherein a second of said fluid parameters comprises the phosphate concentration of the boiler fluid and wherein said input means further comprises means for determining the phosphate concentration of the boiler fluid.
7. The control system of claim 6 wherein said means for determining the phosphate concentration of the boiler fluid provides an on-line measurement of the phosphate concentration of the boiler fluid.
8. The control system of claim 6 wherein said target region comprises a desired predetermined ratio of the sodium concentration in the boiler fluid to the phosphate concentration in the boiler fluid.
9. The control system of claim 8 wherein said target region has an upper ratio control limit, a lower ratio control limit, an upper phosphate control limit and a lower phosphate control limit, said limits forming a closed region, said target region having target region vertices.
10. The control system of claim 9 wherein said control means further comprises an adaptive controller, said adaptive controller comprising a model of the industrial boiler for determining said respectively determined feed rates of said first and second fluid treatment materials.
11. The control system of claim 10 wherein said adaptive controller comprises means for accounting for dead time of the boiler during boiler operation.
12. The control system of claim 10 wherein said adaptive controller further comprises means for updating said model responsive to said input means.
13. The control system of claim 12 wherein said input means comprises a blowdown flowmeter for providing an on-line measurement of the blowdown flow.
14. The control system of claim 12 wherein said adaptive controller further comprises means for calculating the blowdown flow.
15. The control system of claim 14 wherein said adaptive controller further comprises means for calculating the sodium concentration in the boiler fluid.
16. The control system of claim 15 wherein said input means further comprises means for measuring ammonia in the boiler fluid and wherein said means for calculating the sodium concentration in the boiler fluid accounts for the ammonia in the boiler fluid.
17. The control system of claim 16 wherein said adaptive controller further comprises means for determining a feedwater contaminant ingress.
18. The control system of claim 15 wherein said adaptive controller further comprises means for determining a feedwater contaminant ingress.
19. The control system of claim 18 wherein said means for calculating the blowdown flow and said means for calculating a feedwater contaminant ingress are based on a series of phosphate and pH measurements in an industrial boiler system using small sample intervals.
20. The control system of claim 18 wherein said adaptive controller further comprises means for developing a pumpable region that defines a first range of steady state boiler fluid sodium and phosphate concentrations that are attainable from the current boiler fluid sodium and phosphate concentrations using a second range of first feedstream feed rates and second feedstream feed rates, said pumpable region having pumpable region vertices and a pumpable region perimeter.
21. The control system of claim 20 wherein said adaptive controller further comprises means for analyzing said target region, said pumpable region, and said current boiler fluid sodium and phosphate concentrations in a boiler state space.
22. The control system of claim 21 wherein said adaptive controller comprises means for determining that said target region cannot be reached from said boiler state space by any feed program.
23. The control system of claim 22 wherein said means for determining that said target region cannot be reached warns the operator and implements a feed program that will drive said current boiler fluid concentrations towards an area of said pumpable region that is closest to said target region.
24. The control system of claim 21 wherein said means for analyzing determines an optimum feed program, said optimum feed program controlling said respectively determined feed rates of said first and second feedstreams in order to achieve said target region within the boiler fluid in the least amount of time.
25. The control system of claim 24 wherein said optimum feed program comprises first means for bringing said current boiler fluid concentrations within said target region in the least amount of time thereby establishing new boiler fluid concentrations and second means for bringing said new boiler fluid concentrations to a predetermined setpoint within said target region in the least amount of time.
26. The control system of claim 25 wherein said optimum feed program further comprises means for maintaining said respectively determined feed rates corresponding to said setpoint once said setpoint has been achieved.
27. The control system of claim 25 wherein said first means for bringing said current boiler fluid concentrations within said target region in the least amount of time comprises means for analyzing a first set of feed rate trajectories that form lines between the current boiler fluid concentrations and said pumpable region vertices, and a second set of feed rate trajectories that form lines between the current boiler fluid concentrations and said target region vertices, said second set of feed rate trajectories being projected until they intersect said pumpable region perimeter if at all, thereby defining a third set of feed rate trajectories.
28. The control system of claim 27 wherein said means for analyzing selects one feed rate trajectory from said first and said third set of feed rate trajectories that reaches said target region in the least amount of time.
29. The control system of claim 25 wherein said second means for bringing said new boiler fluid concentrations to said setpoint in the least amount of time comprises means for determining the feed rate trajectory that coincides with a line formed between said new boiler fluid concentrations and said setpoint, said feed rate trajectory being projected until it intersects said pumpable region perimeter, if at all.
30. The control system of claim 25 wherein said second means for bringing said new current boiler fluid concentrations to said setpoint in the least amount of time comprises means for establishing a new target region around said setpoint and having new target region vertices.
31. The control system of claim 30 wherein said second means for bringing said new boiler fluid concentrations to said setpoint in the least amount of time further comprises means for analyzing a fourth set of feed rate trajectories that form lines between the new boiler fluid concentrations and said pumpable region vertices, and a fifth set of feed rate trajectories that form lines between the new boiler fluid concentrations and said new target region vertices, said fifth set of feed rate trajectories being projected until they intersect said pumpable region perimeter if at all, thereby defining a sixth set of feed rate trajectories.
32. The control system of claim 31 wherein means for analyzing selects one feed rate trajectory from said fourth and said sixth set of feed rate trajectories that reaches said new target region in the least amount of time, thereby establishing newer boiler fluid concentrations.
33. The control system of claim 32 wherein said second means further comprises means for repeating the establishment of additional target regions and corresponding feed rate trajectories based on said newer boiler fluid concentrations, said means for repeating operating until said setpoint is achieved.
34. A method for controlling at least two interdependent chemicals in the fluid of a continuously stirred tank reactor system having an effluent flow, said method comprising the steps of:
(a) establishing a mathematical model of said continuously stirred tank reactor system;
(b) monitoring the concentration of one of said at least two interdependent chemicals in the fluid, the pH of the fluid and the temperature at which the pH is measured;

(c) updating the model based on the concentration of said one of said at least two interdependent chemicals in the fluid, the pH of said fluid and the temperature at which the pH is measured;
(d) 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 to the fluid at respective feed rates; and (e) developing an optimum feed rate program for controlling said feedstreams to automatically achieve and maintain a setpoint of said at least two interdependent chemicals in the fluid of the continuously stirred tank reactor system in the least amount of time from the current concentrations of said chemicals in the fluid.
35. The method of claim 34 wherein said step of developing an optimum feed rate program includes generating a pumpable region in a system state space that defines a first range of all steady state fluid concentrations of said at least two interdependent chemicals in the fluid that are attainable from the current concentrations of said at least two interdependent chemicals in the fluid depending on a second range of first feedstream feed rates and second feedstream feed rates, said pumpable region having pumpable region vertices and a pumpable region perimeter that corresponds to said second range of first feedstream feed rates and second feedstream feed rates.
36. The method of claim 35 wherein said step of developing an optimum feed rate program further includes:
(a) defining a new target region about said setpoint and having new target region vertices;
(b) determining the time required to drive the new concentrations of said at least two interdependent chemicals in the fluid along a-fourth set of feed rate trajectories formed between the new concentrations of said at least two interdependent chemicals and said pumpable region vertices;
(c) determining the time required to drive the new concentrations of said at least two interdependent chemicals in the fluid along a fifth set of feed rate trajectories formed between the new concentrations of said at least two interdependent chemicals and said new target region vertices, said fifth set of feed rate trajectories being projected until they intersect said pumpable region perimeter if at all, thereby defining a sixth set of feed rate trajectories;
(d) selecting one feed rate trajectory from said fourth and sixth set of feed rate trajectories that requires the least amount of time to reach said new target region and then pumping said first and second feedstreams along said selected feed rate trajectory, thereby establishing newer concentrations of said at least two interdependent chemicals in the fluid;
(e) repeating steps a-d until said setpoint is achieved; and (f) maintaining the feed rates of said first and said second feedstreams that correspond to said setpoint when said setpoint is achieved.
37. The method of claim 35 wherein said step of developing an optimum feed rate program further includes defining a target region about said setpoint of said at least two interdependent chemicals, said target region having target region vertices.
38. The method of claim 37 wherein said step of developing an optimum feed rate program further includes determining the time required to drive the current concentrations of said at least two interdependent chemicals in the fluid along a first set of feed rate trajectories formed between the current concentrations of said at least two interdependent chemicals and said pumpable region vertices.
39. The method of claim 38 wherein said step of developing an optimum feed rate program further includes determining the time required to drive the current concentrations of said at least two interdependent chemicals in the fluid along a second set of feed rate trajectories formed between the current concentrations of said at least two interdependent chemicals and said target region vertices, said second set of feed rate trajectories being projected until they intersect said pumpable region perimeter if at all, thereby defining a third set of feed rate trajectories.
40. The method of claim 39 wherein said step of developing an optimum feed rate program further includes selecting one feed rate trajectory from said first and third set of feed rate trajectories that requires the least amount of time to reach said target region and then pumping said first and second feedstreams along said selected feed rate trajectory, thereby establishing new concentrations of said at least two interdependent chemicals in the fluid.
41. The method of claim 40 wherein said step of developing an optimum feed rate program further includes determining a second feed rate trajectory that coincides with a line formed between the new concentrations of said at least two interdependent chemicals in the fluid and said setpoint, said second feed rate trajectory being projected until it intersects said pumpable region perimeter, if at all.
42. The method of claim 41 wherein said step of developing an optimum feed rate program further includes maintaining said respective feed rates of said first and second feedstreams that correspond to said setpoint when said setpoint has been achieved.
43. The method of claim 34 wherein said continuously stirred tank reactor system is an industrial boiler having a boiler fluid and said effluent flow is a blowdown flow.
44. The method of claim 43 wherein said one of said at least two interdependent chemicals is phosphate.
45. The method of claim 44 wherein said one of said at least two interdependent chemicals is sodium.
46. The method of claim 45 wherein said method further includes the step of estimating the blowdown flow.
47. The method of claim 46 wherein said method further includes the steps of calculating the phosphate concentration and the sodium concentration in the boiler fluid.
48. The method of claim 47 wherein said method further includes the step of estimating a feedwater contaminant ingress.
49. The method of claim 48 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 the system uses small sample intervals.
50. The method of claim 47 wherein said step of calculating the sodium concentration includes a step for measuring ammonia in the boiler fluid and accounting for the ammonia in determining the sodium concentration.
51. The method of claim 50 wherein said method further includes the step of estimating a feedwater contaminant ingress.
52. The method of claim 43 wherein said method further includes measuring the blowdown flow to obtain blowdown flow values.
53. The method of claim 52 wherein said one of said at least two interdependent chemicals is phosphate.
54. The method of claim 53 wherein said one of said at least two interdependent chemicals is sodium.
55. The method of claim 54 wherein said method further includes the steps of calculating the phosphate concentration and the sodium concentration in the boiler fluid.
56. The method of claim 55 wherein said method further includes the step of estimating a feedwater contaminant ingress.
57. The method of claim 56 wherein said method further includes calculating a boiler phosphate mass imbalance.
58. The method of claim 54 wherein said method further includes providing on-line measurement of the pH of the boiler fluid and on-line measurement of the phosphate concentration in the boiler fluid to provide pH values and phosphate concentration values.
59. The method of claim 58 wherein said method further includes calculating a feedwater contaminant ingress.
60. The method of claim 34 wherein said method further includes a step that accounts for dead time in the continuously stirred tank reactor system.
61. A system for controlling the sodium-to-phosphate ratio and the phosphate concentration of a boiler fluid in an industrial boiler having a feedwater flow and a blowdown flow, said system comprising input means for receipt of a boiler fluid parameter and a parameter indicative of said phosphate concentration and control means responsive to said 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 said blowdown flow and independent of the feedwater flow rate without the need to measure any reactant concentration in the feedwater flow.
62. The system of claim 61 wherein said boiler fluid parameter comprises the pH of the boiler fluid, the pH of the boiler fluid being defined by the sodium-to-phosphate ratio and the phosphate concentration and wherein said input means comprises a pH meter for determining the pH value of the boiler fluid and providing the pH value to said control means.
63. The system of claim 62 wherein said input means further comprises a blowdown flowmeter for monitoring the blowdown flow and providing a signal indicative of said blowdown flow, said parameter indicative of said phosphate concentration being calculated from said signal indicative of said blowdown flow.
64. The system of claim 63 wherein said control means comprises a first feedstream and a second feedstream for feeding first and second fluid treatment materials, respectively, to the boiler fluid, said first material comprising a mixture of sodium and phosphate having a first predetermined sodium-to-phosphate ratio and a first predetermined concentration of phosphate, said second material comprising a mixture of sodium and phosphate having a second predetermined sodium-to-phosphate ratio and a second predetermined concentration of phosphate.
65. The system of claim 64 wherein said first predetermined concentration of phosphate and said second predetermined concentration of phosphate are identical.
66. The system of claim 65 wherein said control means includes maintenance means for maintaining said predetermined fixed phosphate concentration in the boiler fluid, said maintenance means being arranged to deliver said first and second fluid treatment materials to the boiler fluid in proportion to the blowdown flow.
67. The system of claim 66 wherein said control means operates upon a pH setpoint, said pH setpoint being defined by the predetermined desired sodium-to-phosphate ratio and the predetermined fixed phosphate concentration, and wherein said control means further comprises comparator means for comparing the pH value of the boiler fluid with the pH
setpoint.
68. The system of claim 67 wherein said maintenance means feeds a selected one but not the other of said first and second fluid treatment materials from its associated feedstream into the boiler fluid at one time, said selected one being selected depending upon whether the pH value is greater than or less than the pH setpoint.
69. The system of claim 68 wherein said input means further comprises a phosphate analyzer for determining a boiler phosphate mass imbalance.
70. A system for maintaining a fixed concentration of a fluid treatment material in the boiler water of an industrial boiler having a blowdown flow and a feedwater flow, said system comprising at least one feedstream for delivering said fluid treatment material to the boiler fluid, said at least one feedstream delivering said fluid treatment material in proportion to said blowdown flow but without controlling the blowdown flow, said system operating independent of the feedwater flow rate without the need to measure any reactant concentration in the feedwater flow.
71. A method for controlling the sodium-to-phosphate ratio and the phosphate concentration of a boiler fluid in an industrial boiler having a blowdown flow and a feedwater flow, said method comprising the steps of:
(a) selecting a predetermined fixed phosphate concentration and a predetermined desired sodium-to-phosphate ratio;
(b) monitoring a boiler fluid parameter;
(c) comparing said boiler fluid parameter with a preestablished setpoint;
(d) monitoring a parameter indicative of said phosphate concentration;
(e) providing a supply of a first sodium phosphate fluid treatment material, said first sodium phosphate fluid treatment material having a first predetermined sodium-to-phosphate ratio and a first known phosphate concentration;
(f) providing a supply of a second sodium phosphate fluid treatment material, said second sodium phosphate fluid treatment material having a second predetermined sodium-to-phosphate ratio and a second known phosphate concentration;

(g) automatically feeding said first sodium phosphate fluid treatment material to the boiler fluid when said monitored parameter is less than or equal to said pre-established setpoint and automatically feeding said second sodium phosphate fluid treatment material to the boiler fluid when said monitored parameter is greater than said pre-established setpoint, said first sodium phosphate fluid treatment material and said second sodium phosphate fluid treatment material defining a selected sodium phosphate fluid treatment material whenever one of these is being automatically fed; and (h) said selected fluid treatment material being fed, in proportion to the blowdown flow without controlling the blowdown flow and independent of the feedwater flow rate without the need to measure any reactant concentration in the feedwater flow, to the boiler fluid based on said parameter indicative of said phosphate concentration to automatically achieve and maintain said predetermined fixed phosphate concentration.
72. The method of claim 71 wherein said monitored parameter is the pH of the boiler fluid.
73. The method of claim 72 wherein said first known phosphate concentration is identical to said second known phosphate concentration.
74. A method for maintaining a fixed concentration of a fluid treatment material in the boiler fluid of an industrial boiler having a blowdown flow and a feedwater flow, said method comprising the steps of:
(a) selecting a predetermined fluid treatment material concentration for the boiler fluid;
(b) providing a supply of the predetermined fluid treatment material having a known concentration;
(c) monitoring the blowdown flow;

(d) feeding the fluid treatment material to the boiler fluid in proportion to the blowdown flow to automatically achieve and maintain the predetermined fluid treatment material concentration in the boiler fluid but without controlling the blowdown flow independent of the feedwater flow rate and without the need to measure any reactant concentration in the feedwater flow.
CA002156880A 1994-10-11 1995-08-24 Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler Expired - Lifetime CA2156880C (en)

Priority Applications (1)

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

Applications Claiming Priority (2)

Application Number Priority Date Filing Date Title
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
US08/321,338 1994-10-11

Related Child Applications (1)

Application Number Title Priority Date Filing Date
CA002415685A Division CA2415685C (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
CA2156880A1 CA2156880A1 (en) 1996-04-12
CA2156880C true CA2156880C (en) 2003-12-09

Family

ID=23250189

Family Applications (1)

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

Country Status (2)

Country Link
US (1) US5696696A (en)
CA (1) CA2156880C (en)

Families Citing this family (21)

* Cited by examiner, † Cited by third party
Publication number Priority date Publication date Assignee Title
US5724254A (en) * 1996-01-18 1998-03-03 Electric Power Research Institute Apparatus and method for analyzing power plant water chemistry
US6244098B1 (en) 1997-02-13 2001-06-12 Betzdearborn Inc. Methods and apparatus for monitoring water process equipment
US5817927A (en) * 1997-04-11 1998-10-06 Betzdearborn Inc. Method and apparatus for monitoring water process equipment
US6170319B1 (en) 1998-03-31 2001-01-09 Betzdearborn Inc. Methods and apparatus for monitoring water process equipment
US6556930B1 (en) 1998-06-19 2003-04-29 Rodi Systems Corp. Fluid treatment apparatus
US6609070B1 (en) 1998-06-19 2003-08-19 Rodi Systems Corp Fluid treatment apparatus
US6473480B1 (en) * 1999-12-30 2002-10-29 General Electric Company Method and apparatus for maintaining proper noble metal loading for a noble metal application process for water-cooled nuclear reactors
US20050228511A1 (en) * 2002-01-15 2005-10-13 Suvajit Das Computer-implemented system and method for measuring and improving manufacturing processes and maximizing product research and development speed and efficiency
US7376472B2 (en) * 2002-09-11 2008-05-20 Fisher-Rosemount Systems, Inc. Integrated model predictive control and optimization within a process control system
US7104115B2 (en) * 2004-05-07 2006-09-12 Sensicore, Inc. Fluid treatment apparatus with input and output fluid sensing
US20050251366A1 (en) * 2004-05-07 2005-11-10 Sensicore, Inc. Monitoring systems and methods for fluid testing
US20060020427A1 (en) * 2004-05-07 2006-01-26 Sensicore, Inc. Systems and methods for fluid quality monitoring using portable sensors in connection with supply and service entities
US7100427B2 (en) * 2004-05-07 2006-09-05 Sensicore, Inc. Multi-sensor system for fluid monitoring with selective exposure of sensors
US7249000B2 (en) * 2004-05-07 2007-07-24 Sensicore, Inc. Fluid monitoring systems and methods with data communication to interested parties
WO2006135849A2 (en) * 2005-06-10 2006-12-21 Sensicore, Inc. Systems and methods for fluid quality sensing, data sharing and data visualization
US7451004B2 (en) 2005-09-30 2008-11-11 Fisher-Rosemount Systems, Inc. On-line adaptive model predictive control in a process control system
US9057484B2 (en) 2010-08-11 2015-06-16 Huguenot Laboratories Bypass feeder device
CN107601632B (en) * 2017-10-30 2023-06-02 清华大学深圳研究生院 Automatic dosing control method and system for coagulation
RU2724451C1 (en) * 2020-01-14 2020-06-23 Иван Андреевич Тихонов Method for control and adjustment of water-chemical mode of steam boiler
CN113461221B (en) * 2021-07-28 2023-06-06 丰城市天壕新能源有限公司 Intelligent dosing system for dry quenching waste heat power generation
CN115677015B (en) * 2023-01-03 2023-04-07 江苏江南环境工程设计院有限公司 Wastewater treatment process and recycling method based on precise control

Family Cites Families (41)

* Cited by examiner, † Cited by third party
Publication number Priority date Publication date Assignee Title
US3011709A (en) * 1955-03-24 1961-12-05 Honeywell Regulator Co Computer apparatus for rapidly changing the value of a process variable
US2842311A (en) * 1955-03-24 1958-07-08 Honeywell Regulator Co Control apparatus
US3462364A (en) * 1966-01-17 1969-08-19 Herbert Gustaf Carlson Method and apparatus for optimizing chemical treatment
US3891836A (en) * 1972-04-21 1975-06-24 Mobil Oil Corp Apparatus for optimizing multiunit processing systems
US3792244A (en) * 1972-06-02 1974-02-12 Fridrich Uhde Gmbh Circuit for ph value regulation
US3804253A (en) * 1973-01-29 1974-04-16 Waterguard Syst Inc System for automatically maintaining chlorine concentration and ph of swimming pool water at predetermined levels
JPS6037919B2 (en) * 1974-12-25 1985-08-29 株式会社東芝 Automatic operation control equipment for nuclear power plants
US4016079A (en) * 1975-09-16 1977-04-05 Aquasol, Inc. Automatic chlorine and pH control apparatus for swimming pools
US4033871A (en) * 1975-11-13 1977-07-05 Paddock Of California, Inc. Integrated monitor and control system for continuously monitoring and controlling pH and free halogen in swimming pool water
US4053743A (en) * 1976-02-18 1977-10-11 Antti Johannes Niemi Method for controlling the ph and other concentration variables
FI55414C (en) * 1977-06-07 1979-07-10 Niemi Antti Johannes FOERFARANDE OCH ANORDNING FOER REGLERING AV PH
US4181951A (en) * 1978-04-28 1980-01-01 Jan Boeke In-line pH and pIon controller
US4349869A (en) * 1979-10-01 1982-09-14 Shell Oil Company Dynamic matrix control method
US4644479A (en) * 1984-07-31 1987-02-17 Westinghouse Electric Corp. Diagnostic apparatus
US4659459A (en) * 1985-07-18 1987-04-21 Betz Laboratories, Inc. Automated systems for introducing chemicals into water or other liquid treatment systems
US4736316A (en) * 1986-08-06 1988-04-05 Chevron Research Company Minimum time, optimizing and stabilizing multivariable control method and system using a constraint associated control code
US4833622A (en) * 1986-11-03 1989-05-23 Combustion Engineering, Inc. Intelligent chemistry management system
US4770843A (en) * 1987-04-08 1988-09-13 Westinghouse Electric Corp. Controlling fuel assembly stability in a boiling water reactor
DE3729270A1 (en) * 1987-09-02 1989-03-16 Henkel Kgaa COMPACT STATION FOR COOLING CIRCUIT TREATMENT
IE64511B1 (en) * 1988-03-11 1995-08-09 Takeda Chemical Industries Ltd Automated synthesizing apparatus
US4897797A (en) * 1988-04-25 1990-01-30 Betz Laboratories, Inc. Proportional chemical feeding system
US5038270A (en) * 1988-04-26 1991-08-06 Mitsubishi Kasei Corporation Method for controlling reactor system
US5040725A (en) * 1988-12-20 1991-08-20 Butler Warren E Adaptive controller for forced hot water heating systems
US5083281A (en) * 1989-05-12 1992-01-21 Bell & Howell Phillipsburg Co. Insertion machine with speed optimization
US5288713A (en) * 1989-08-16 1994-02-22 Nalco Chemical Company Method for injecting treatment chemicals
US5141716A (en) * 1989-10-25 1992-08-25 Betz Laboratories, Inc. Method for mitigation of caustic corrosion in coordinated phosphate/ph treatment programs for boilers
FR2654849A1 (en) * 1989-11-23 1991-05-24 Atochem TECHNIQUE FOR DETERMINING THE OPTIMAL RELATION VARIABLES / PROPERTIES IN A METHOD AND / OR COMPOSITION.
US5132916A (en) * 1990-05-21 1992-07-21 Elsag International B.V. Methodology for ph titration curve estimation for adaptive control
US5248577A (en) * 1990-08-13 1993-09-28 Eastman Kodak Company Reactant concentration control method and apparatus for precipitation reactions
EP0524317A4 (en) * 1991-02-08 1995-02-15 Tokyo Shibaura Electric Co Model forecasting controller
US5262963A (en) * 1991-06-28 1993-11-16 Imc Fertilizer, Inc. Automatic control system for phosphoric acid plant
US5152252A (en) * 1992-01-23 1992-10-06 Autotrol Corporation Water treatment control system for a boiler
US5568377A (en) * 1992-10-29 1996-10-22 Johnson Service Company Fast automatic tuning of a feedback controller
FR2700026B1 (en) * 1992-12-30 1995-02-10 Framatome Sa Method and device for regulating a process.
US5320967A (en) * 1993-04-20 1994-06-14 Nalco Chemical Company Boiler system leak detection
CA2118885C (en) * 1993-04-29 2005-05-24 Conrad K. Teran Process control system
US5424942A (en) * 1993-08-10 1995-06-13 Orbital Research Inc. Extended horizon adaptive block predictive controller with an efficient prediction system
US5587897A (en) * 1993-12-27 1996-12-24 Nec Corporation Optimization device
US5486995A (en) * 1994-03-17 1996-01-23 Dow Benelux N.V. System for real time optimization
US5516423A (en) * 1994-08-08 1996-05-14 Concorp, Inc. Variable residence time treatment system
US5519605A (en) * 1994-10-24 1996-05-21 Olin Corporation Model predictive control apparatus and method

Also Published As

Publication number Publication date
US5696696A (en) 1997-12-09
CA2156880A1 (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
Wittenmark et al. Practical issues in the implementation of self-tuning control
Morozan Stabilization of some stochastic discrete–time control systems
Owens An Updated set of parton distribution parametrizations
US20230386314A1 (en) Methods for indoor gas leakage disposal of smart gas and internet of things systems thereof
US20010021900A1 (en) Robust steady-state target calculation for model predictive control
Veres et al. Predictive self-tuning control by parameter bounding and worst-case design
Kalafatis et al. Linearizing feedforward–feedback control of pH processes based on the Wiener model
LIND The design of structural design norms
CA2415685C (en) Apparatus and method for automatically achieving and maintaining congruent control in an industrial boiler
WU et al. Robust stabilization of uncertain linear dynamical systems
Gendron et al. Deterministic adaptive control of SISO processes using model weighting adaptation
US5923571A (en) Apparatus and method for automatic congruent control of multiple boilers sharing a common feedwater line and chemical feed point
Nagar et al. LFT/SDP approach to the uncertainty analysis for state estimation of water distribution systems
Lee et al. On-line optimal control of induced foreign protein production by recombinant bacteria in fed-batch reactors
Huang et al. Self-tuning pH control in Dyeing
Little et al. Predictive control using constrained optimal control
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
Lennon et al. Strategies for genetic adaptive control
Yumoto et al. An approach to automatic model generation for stochastic qualitative simulation of building air conditioning systems
Kitamura et al. Identifiability of dynamics of a boiling water reactor using autoregressive modeling
Rami et al. Control of jump linear systems: Application to the steam generator water level
Xiao Sensor fault detection and diagnosis of air handling units
Bendotti et al. Modelling and control of a pressurized water reactor using μ-synthesis techniques
Guillermard et al. pms-1—A real-time advisor for the boron control in PWR nuclear reactors

Legal Events

Date Code Title Description
EEER Examination request
MKEX Expiry

Effective date: 20150824