Is there a more memory-efficient way to write a linearizer FB?

strantor

Member
Join Date
Sep 2010
Location
katy tx
Posts
401
Hello, I would like to see if anyone can give me some pointers on how to optimize this function block that I have written for a Omron CJ2M. It is just simple piecewise line formula, but the only way I know to handle the piecewise portion is with a slew of if-then statements, which is gobbling up more memory than I can spare. This FB consumes 989 steps and 78 non-retain memory locations! As you can see, I'm not the text-based programming savant. please help!

Code:
(* 
This function block takes a bipolar floating point -100 to +100 reference with zero 
deadband and converts it to a unipolar integer reference with deadband compensation
and multipoint mapping linearization for a bidirectional analog proportional flow control
valve with nonlinear output.
(EX: 2500 [full reverse] to 7500 [full forward] with deadband [zero speed] from 4000 to 6000) 

(*define variables and constants:*)
X1_Neg100:= -100.0;
Y1_NEG100Float:= INT_TO_REAL(NEG100);
X2_Neg80:= -80.0;
Y2_NEG80Float:= INT_TO_REAL(NEG80);
X3_Neg60:= -60.0;
Y3_NEG60Float:= INT_TO_REAL(NEG60);
X4_Neg40:= -40.0;
Y4_NEG40Float:= INT_TO_REAL(NEG40);
X5_Neg20:= -20.0;
Y5_NEG20Float:= INT_TO_REAL(NEG20);
X6_Neg1:= -1.0;
Y6_NEG1Float:= INT_TO_REAL(NEG1);
X7_Pos1:= +1.0;
Y7_POS1Float:= INT_TO_REAL(POS1);
X8_Pos20:= +20.0;
Y8_POS20Float:= INT_TO_REAL(POS20);
X9_Pos40:= +40.0;
Y9_POS40Float:= INT_TO_REAL(POS40);
X10_Pos60:= +60.0;
Y10_POS60Float:= INT_TO_REAL(POS60);
X11_Pos80:= +80.0;
Y11_POS80Float:= INT_TO_REAL(POS80);
X12_Pos100:= +100.0;
Y12_POS100Float:= INT_TO_REAL(POS100);

(*INPUT CONDITIONING - invert incoming value an apply speed limit if speed limit if desired*)
IF Invert = TRUE THEN
	IF SpeedLimit = TRUE THEN
		InputFloat_X:= Input_float100*(-1.0)*ScalingFactor*(0.5);
	ELSE
		InputFloat_X:= Input_float100*(-1.0)*ScalingFactor;
	END_IF;
ELSE
	IF SpeedLimit = TRUE THEN
		InputFloat_X:= Input_float100*ScalingFactor*(0.5);
	ELSE 
		InputFloat_X:= Input_float100*ScalingFactor;
	END_IF;
END_IF;

(*calculate slope and y-intercept of line segments for positive range and negative range:*)
IF Input_float100 > X11_Pos80 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y12_POS100Float-Y11_POS80Float)/(X12_Pos100-X11_Pos80);
	Y_Int_B:= Y12_POS100Float-(M_Slope*X12_Pos100);
ELSIF  Input_float100 < X11_Pos80 AND Input_float100 > X10_Pos60 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y11_POS80Float-Y10_POS60Float)/(X11_Pos80-X10_Pos60);
	Y_Int_B:= Y11_POS80Float-(M_Slope*X11_Pos80);
ELSIF  Input_float100 < X10_Pos60 AND Input_float100 > X9_Pos40 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y10_POS60Float-Y9_POS40Float)/(X10_Pos60-X9_Pos40);
	Y_Int_B:= Y10_POS60Float-(M_Slope*X10_Pos60);
ELSIF  Input_float100 < X9_Pos40 AND Input_float100 > X8_Pos20 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y9_POS40Float-Y8_POS20Float)/(X9_Pos40-X8_Pos20);
	Y_Int_B:= Y9_POS40Float-(M_Slope*X9_Pos40);
ELSIF  Input_float100 < X8_Pos20 AND Input_float100 > X7_Pos1 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y8_POS20Float-Y7_POS1Float)/(X8_Pos20-X7_Pos1);
	Y_Int_B:= Y8_POS20Float-(M_Slope*X8_Pos20);
ELSIF  Input_float100 < X7_Pos1 AND Input_float100 > X6_Neg1 THEN
	ValveCenter:= TRUE;
ELSIF  Input_float100 < X6_Neg1 AND Input_float100 > X5_Neg20 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y6_NEG1Float-Y5_NEG20Float)/(X6_Neg1-X5_Neg20);
	Y_Int_B:= Y6_NEG1Float-(M_Slope*X6_Neg1);
ELSIF  Input_float100 < X5_Neg20 AND Input_float100 > X4_Neg40 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y5_NEG20Float-Y4_NEG40Float)/(X5_Neg20-X4_Neg40);
	Y_Int_B:= Y5_NEG20Float-(M_Slope*X5_Neg20);
ELSIF  Input_float100 < X4_Neg40 AND Input_float100 > X3_Neg60 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y4_NEG40Float-Y3_NEG60Float)/(X4_Neg40-X3_Neg60);
	Y_Int_B:= Y4_NEG40Float-(M_Slope*X4_Neg40);
ELSIF  Input_float100 < X3_Neg60 AND Input_float100 > X2_Neg80 THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y3_NEG60Float-Y2_NEG80Float)/(X3_Neg60-X2_Neg80);
	Y_Int_B:= Y3_NEG60Float-(M_Slope*X3_Neg60);
ELSIF  Input_float100 < X2_Neg80  THEN
	ValveCenter:= FALSE;
	M_Slope:= (Y2_NEG80Float-Y1_NEG100Float)/(X2_Neg80-X1_Neg100);
	Y_Int_B:= Y2_NEG80Float-(M_Slope*X2_Neg80);
END_IF;

(*write valve output*)
IF ValveCenter= TRUE THEN
		OutputFloat_Y:= +5000.0;
ELSE
		OutputFloat_Y:= M_Slope*InputFloat_X+Y_Int_B;	
END_IF;
OutputInt:= REAL_TO_INT(OutputFloat_Y);

I see one obvious thing: defining constants like "X2_Neg80:= -80.0;" and then using X2_Neg80 in the code when I could just use -80.0, but I don't think that's the bulk of the problem. I think the problem is deeper and there is an entirely different, better way to write the code from the ground up. I just don't know what that might be.
 
Not sure if I write it right way, but at least little bit less variables:

(*
This function block takes a bipolar floating point -100 to +100 reference with zero
deadband and converts it to a unipolar integer reference with deadband compensation
and multipoint mapping linearization for a bidirectional analog proportional flow control
valve with nonlinear output.
(EX: 2500 [full reverse] to 7500 [full forward] with deadband [zero speed] from 4000 to 6000)

IF input_float <=-80 then
X1_float = -100
Y1_float = Int_to_real(NEG100)
X2_float = -80
Y2_float = int_to real(NEG80)
END_IF;

IF input_float <=-60 then
X1_float = -80
Y1_float = Int_to_real(NEG80)
X2_float = -60
Y2_float = int_to real(NEG60)
END_IF;

IF input_float <=-40 then
X1_float = -60
Y1_float = Int_to_real(NEG60)
X2_float = -40
Y2_float = int_to real(NEG40)
END_IF;

IF input_float <=-20 then
X1_float = -40
Y1_float = Int_to_real(NEG40)
X2_float = -20
Y2_float = int_to real(NEG20)
END_IF;

IF input_float <=-1 then
X1_float = -1
Y1_float = Int_to_real(NEG40)
X2_float = +1
Y2_float = int_to real(POS1)
END_IF;

IF input_float >=1 then
X1_float = +1
Y1_float = Int_to_real(POS1)
X2_float = +20
Y2_float = int_to real(POS20)
END_IF;

IF input_float >=20 then
X1_float = +20
Y1_float = Int_to_real(POS20)
X2_float = +40
Y2_float = int_to real(POS40)
END_IF;

IF input_float >=40 then
X1_float = +40
Y1_float = Int_to_real(POS40)
X2_float = +60
Y2_float = int_to real(POS60)
END_IF;

IF input_float >=60 then
X1_float = +60
Y1_float = Int_to_real(POS60)
X2_float = +80
Y2_float = int_to real(POS80)
END_IF;

IF input_float >=80 then
X1_float = +80
Y1_float = Int_to_real(POS80)
X2_float = +100
Y2_float = int_to real(POS100)
END_IF;


(*INPUT CONDITIONING - invert incoming value an apply speed limit if speed limit if desired*)
IF Invert = TRUE THEN
IF SpeedLimit = TRUE THEN
InputFloat_X:= Input_float100*(-1.0)*ScalingFactor*(0.5);
ELSE
InputFloat_X:= Input_float100*(-1.0)*ScalingFactor;
END_IF;
ELSE
IF SpeedLimit = TRUE THEN
InputFloat_X:= Input_float100*ScalingFactor*(0.5);
ELSE
InputFloat_X:= Input_float100*ScalingFactor;
END_IF;
END_IF;

if Input_float100 < +1 AND Input_float100 > -1 THEN
ValveCenter:= TRUE;
else
ValveCenter:= false;
END_IF;

If valvecenter=false Then
M_slope:=(Y2_Float-Y1Float)/(X2_float-X1_float);
Y_Int_B=Y2_Float-(M_Slope*X2_float);
OutputFloat_Y:= M_Slope*InputFloat_X+Y_Int_B;
else

OutputFloat_Y:= +5000.0;
END_IF;
OutputInt:= REAL_TO_INT(OutputFloat_Y);




With indirect addressing + counter you maybe can write this little bit effisient...
 
with indirect addressing something like this, if you can use indirect addressing on PLC

Y_values array of int [0..11]
Y_value[0]=NEG100
Y_value[1]=NEG80
Y_value[2]=NEG60
Y_value[3]=NEG40
Y_value[4]=NEG20
Y_value[5]=NEG1
Y_value[6]=POS1
Y_value[7]=POS20
Y_value[8]=POS40
Y_value[9]=POS60
Y_value[10]=POS80
Y_value[11]=POS100

index_counter = index_counter+1;
if index_counter>10 then
index_counter = 0
end_if;
index_counter_plus_one:=index_counter+1;
index_value:=index_counter*20-100;


IF input_float <=index_value then
X1_float = index_value
Y1_float = Int_to_real(Y_value[index_counter])
X2_float = index_value+20
Y2_float = int_to real(Y_value[index_counter_plus_one]);
END_IF;

If X2_float= 0 then X2_float:=-1 end_if;
if X1_float= 0 then X1_float:=+1 end_if;


(*INPUT CONDITIONING - invert incoming value an apply speed limit if speed limit if desired*)
IF Invert = TRUE THEN
IF SpeedLimit = TRUE THEN
InputFloat_X:= Input_float100*(-1.0)*ScalingFactor*(0.5);
ELSE
InputFloat_X:= Input_float100*(-1.0)*ScalingFactor;
END_IF;
ELSE
IF SpeedLimit = TRUE THEN
InputFloat_X:= Input_float100*ScalingFactor*(0.5);
ELSE
InputFloat_X:= Input_float100*ScalingFactor;
END_IF;
END_IF;

if Input_float100 < +1 AND Input_float100 > -1 THEN
ValveCenter:= TRUE;
else
ValveCenter:= false;
END_IF;

If valvecenter=false Then
M_slope:=(Y2_Float-Y1Float)/(X2_float-X1_float);
Y_Int_B=Y2_Float-(M_Slope*X2_float);
OutputFloat_Y:= M_Slope*InputFloat_X+Y_Int_B;
else

OutputFloat_Y:= +5000.0;
END_IF;
OutputInt:= REAL_TO_INT(OutputFloat_Y);




Problematic is middle part of calculation (you maybe have to calculate +1, -1 parts separetely), but pretty sure that you get basic idea of this...
 
Last edited:
Here's a reduction in the number of multiplies by adding an intermediate variable (SF) and by using negate instead of multiplying by -1

Code:
(*INPUT CONDITIONING - invert incoming value an apply speed limit if speed limit if desired*)
IF SpeedLimit = TRUE THEN
 SF:=ScalingFactor*(0.5);
ELSE
 SF:=ScalingFactor;
END_IF;
InputFloat_X:= Input_float100*SF;
IF Invert TRUE then 
 Input_Float_X:=-InputFloat_X;
END_IF;
 
Thank you gentlemen for your input. I have combined your ideas and with a few of my own tweaks, I have come up with this (611 steps):
Code:
(*map endpoints to X and Y arrays*)
Y_Value[0]:=NEG100;
Y_Value[1]:=NEG80;
Y_Value[2]:=NEG60;
Y_Value[3]:=NEG40;
Y_Value[4]:=NEG20;
Y_Value[5]:=NEG1;
Y_Value[6]:=POS1;
Y_Value[7]:=POS20;
Y_Value[8]:=POS40;
Y_Value[9]:= POS60;
Y_Value[10]:= POS80;
Y_Value[11]:=POS100;
X_Value[0]:= -100.0;
X_Value[1]:= -80.0;
X_Value[2]:= -60.0;
X_Value[3]:= -40.0;
X_Value[4]:= -20.0;
X_Value[5]:= -1.0;
X_Value[6]:= +1.0;
X_Value[7]:= +20.0;
X_Value[8]:= +40.0;
X_Value[9]:= +60.0;
X_Value[10]:= +80.0;
X_Value[11]:= +100.0;

(*INPUT CONDITIONING - invert incoming value an apply speed limit if speed limit if desired*)
IF SpeedLimit = TRUE THEN
	SF:=ScalingFactor*(0.5);
ELSE
	SF:=ScalingFactor;
END_IF;
InputFloat_X:= Input_float100*SF;
IF Invert = TRUE then 
	InputFloat_X:=-InputFloat_X;
END_IF;
(*establish input limits*)
IF InputFloat_X< -100.0 THEN (*negative limit*)
	OutputFloat_Y:= INT_TO_REAL(Y_Value[0]);
ELSIF InputFloat_X > +100.0 THEN 
	OutputFloat_Y:=INT_TO_REAL(Y_Value[11]);(*positive limit*)
ELSIF InputFloat_X < X_Value[6] AND InputFloat_X > X_Value[5] THEN(*center deadband*)
	OutputFloat_Y:=+5000.0;
ELSE 
	(*input range of operation*)
	FOR i:= 0 TO 12 BY 1 DO
		IF InputFloat_X >= X_Value[i] AND InputFloat_X <= X_Value[i+1] THEN (*evaluate which endpoints the input falls between*)
			(*calculate equation of the line between those endpoints*)
			M_Slope:=INT_TO_REAL(Y_Value[i]-Y_Value[i+1])/(X_Value[i]-X_Value[i+1]);
			Y_Intercept:=INT_TO_REAL(Y_Value[i+1])-(M_Slope*X_Value[i+1]);
			OutputFloat_Y:= M_Slope*InputFloat_X+Y_Intercept; 
		END_IF;
	END_FOR;
END_IF;
(*write output*)
OutputInt:= REAL_TO_INT(OutputFloat_Y);
I think this is about as lean as the logic can possibly be (please correct me if I'm wrong). The only further optimization I can see is to eliminate the first section "mapping endpoints." It would be greatly advantageous if I could specify an input as an ARRAY but CX Programmer will only allow internal variables to be specified that way. I am currently investigating using indirect addressing and/or AT commands to achieve it; I know most of the Omron proprietary FBs allow the specification of a "control word," which is the first word, not of an array, but of a series of words that the FB looks at; ex: Control word "n," "n+1," "n+2," etc. - does anyone know how Omron does this? Is it proprietary dark arts? Is there any way for us unwashed to do it as well?
 
maybe these improvements to code?

(*map endpoints to X and Y arrays*)
Y_Value[0]:=NEG100;
Y_Value[1]:=NEG80;
Y_Value[2]:=NEG60;
Y_Value[3]:=NEG40;
Y_Value[4]:=NEG20;
Y_Value[5]:=NEG1;
Y_Value[6]:=POS1;
Y_Value[7]:=POS20;
Y_Value[8]:=POS40;
Y_Value[9]:= POS60;
Y_Value[10]:= POS80;
Y_Value[11]:=POS100;
X_Value[0]:= -100.0;
X_Value[1]:= -80.0;
X_Value[2]:= -60.0;
X_Value[3]:= -40.0;
X_Value[4]:= -20.0;
X_Value[5]:= -1.0;
X_Value[6]:= +1.0;
X_Value[7]:= +20.0;
X_Value[8]:= +40.0;
X_Value[9]:= +60.0;
X_Value[10]:= +80.0;
X_Value[11]:= +100.0;

(*INPUT CONDITIONING - invert incoming value an apply speed limit if speed limit if desired*)
IF SpeedLimit = TRUE THEN
SF:=ScalingFactor*(0.5);
ELSE
SF:=ScalingFactor;
END_IF;
InputFloat_X:= Input_float100*SF;
IF Invert = TRUE then
InputFloat_X:=-InputFloat_X;
END_IF;
(*establish input limits*)
IF InputFloat_X< -100.0 AND INVERSE=FALSE THEN (*negative limit*)
OutputFloat_Y:= INT_TO_REAL(Y_Value[0]);
ELSIF InputFloat_X< -100.0 AND INVERSE=TRUE THEN (*negative limit with inverse*)

OutputFloat_Y:= INT_TO_REAL(Y_Value[11]);
ELSIF InputFloat_X > +100.0 AND INVERSE=FALSE THEN
OutputFloat_Y:=INT_TO_REAL(Y_Value[11]);(*positive limit*)
ELSIF InputFloat_X> +100.0 AND INVERSE=TRUE THEN (*Positive limit with inverse*)
OutputFloat_Y:= INT_TO_REAL(Y_Value[0]);

ELSIF InputFloat_X < X_Value[6] AND InputFloat_X > X_Value[5] THEN(*center deadband*)
OutputFloat_Y:=+5000.0;
ELSE
(*input range of operation*)
FOR i:= 0 TO 10 BY 1 DO
IF InputFloat_X >= X_Value AND InputFloat_X <= X_Value[i+1] THEN (*evaluate which endpoints the input falls between*)
(*calculate equation of the line between those endpoints*)
M_Slope:=INT_TO_REAL(Y_Value-Y_Value[i+1])/(X_Value-X_Value[i+1]);
Y_Intercept:=INT_TO_REAL(Y_Value[i+1])-(M_Slope*X_Value[i+1]);
OutputFloat_Y:= M_Slope*InputFloat_X+Y_Intercept;
END_IF;
END_FOR;
END_IF;
(*write output*)
OutputInt:= REAL_TO_INT(OutputFloat_Y);
 
A comment relative to using FB's in Omron...
Since you have a CJ2M, yhy not use a Structured Text section instead of a Function Block.?
Even if you need multiple instances, ST may be more efficient than FB's in Omron.
The FB implementation has its ok points, but generally is a real memory hog (as you have found out..)
 
If the input doesn't change much from scan to scan I wouldn't use a FOR loop. I would simply use the index from the last time and increment and decrement from there. Most times the index will not change.
If I understand correctly, you refer to something more like what Lare posted in his second reply? I couldn't get that to work, which is why I switched over to the FOR loop. But then again, I couldn't get the FOR loop to work either for some time. Whatever issue I corrected (don't remember what it was) in fussing with the FOR loop might enable to me to fall back to Lare's original code. But if I understand that code correctly (something for which I don't award myself any credit) then I believe it only updates the output every 10 cycles; is that correct?
Isn't linearizing valves fun?
Yes, actually. This is a unique challenge which I have thoroughly enjoyed tackling.

A comment relative to using FB's in Omron...
Since you have a CJ2M, yhy not use a Structured Text section instead of a Function Block.?
Even if you need multiple instances, ST may be more efficient than FB's in Omron.
The FB implementation has its ok points, but generally is a real memory hog (as you have found out..)
That is a good point, and is something I have considered myself. Which brings me to one more question that maybe someone here can answer.

I embarked on this quest to build the ultimate trimmed-down FB because after writing the FB in post#1 I got an error that I was out of UM, and when I checked the memory view I had a negative number in the box below:

attachment.php




So starts the quest, and after slimming down the FB the error did go away, but came back as I added instances of the FB. So I came to realize that I was misguided and the memory issue was not with my FB, but with the rest of my programming. I've trimmed a lot of fat out of my ladder programs, and started writing more FBs to handle some of the more complex operations that were previously handled by overly complicated ladder, as apparently I have loads of memory left for FBs, as shown in memory view:

attachment.php




So as I started migrating over to ST FBs, I began once again getting 'out of memory' errors, but this time in reference to the FB memory area. I got around this the first couple of times by running the "Optimize Function Block/SFC Memory" tool, but then it failed to bail me out any further. So I went and manually re-allocated my FB memory in the HR area from default:

attachment.php


to this:

attachment.php


This has staved off any further memory errors for the time being, but it seems I am bordering on the end of my memory in either direction I choose.

I would like to know where these promised 20480 steps of FB memory in the memory view window are kept, and how to make use of them if not in the default FB section of the HR memory area. I could not find any mention of it in the Omron documentation that I have. I see in the FB Memory Allocation window that I can allocate to other areas, such as DM, but I am timid to make a change there, as the Omron world is not idiot-proof enough for an idiot of my caliber - Omron gives me just enough rope to hang myself, every time.

um1.png um2.png um3.png um4.png
 
I think you are doing this wrong. You want to map -100% to -1% to 2500-4000 , -1% to 1% to 5000 and +1% to +100% to 6000 to 7500

Code:
INPUT = LIMIT(-100.0, INPUT,100.0);
IF -1.0 < INPUT AND < 1.0 THEN
    OUTPUT = 5000;
ELSIF INPUT > 1.0 THEN  (* THE IEC GUYS DON'T KNOW HOW TO SPELL ELSEIF *)
    OUTPUT = (INPUT-1.0)*(7500-6000)/(100-1.0)+6000;
ELSE
    OUTPUT = (INPUT-(-1.0)*(2500-4000))/(-100.0-(-1.0))+4000;
END_IF;
The constants can be simplified. I didn't simplify the math so you can see where the numbers come from.
 
What is a linearizer supposed to do?
In general it is supposed to make a non-linear valve provide flow as if it is linear. That isn't exactly what strantor is really doing since he isn't trying to perfectly compensate for the dead band.

If the system is a dynamic system where the flow must change rapidly then this technique will fail.
 
I did something similar to 'linearising' whereby the output from a weigh scale was not linear. I took weigh readings at 10% incremenets and worked out the error as a percentage. I then averaged the error to get an overall percentage error which I could use as a factor. Although still not 100% accurate this allowed me to get the weights within acceptable limits

Another solution would be a look up table of course
 

Similar Topics

Hi everyone, recently i worked with a cmore panel and have the question that how can clear alarm list whit remote form,right now only can with...
Replies
0
Views
77
Hello, friends, I am trying to upgrade a system that uses an Onrom incremental encoder (E6B2-CWZ6C) connected to a Danfoss VFD (FC360), but now...
Replies
4
Views
249
Hello Friends I have took the sample program from AB webpage and modified, but I can only send 127 chars...
Replies
1
Views
159
Hello, I am new here and have been working with PLCs for a few years now. I have been tasked with setting up a Micrologix 1400….. to a Cmore 10...
Replies
10
Views
448
I am very familiar with Studio 5000 PLC programming. And I'm very familiar with C-More HMI programming. But this is my first time using a C-More...
Replies
2
Views
280
Back
Top Bottom