I am working on creating a mccabe thiele plotter (a chemical engineering topic) which will take some parameters as input and show a plot (using chartjs)
For this, I need to select one among two options 'vol' or 'datapts'. In vol mode (relative volatility mode), equilibrium data equi will be calculated from a equation and for datapts mode (equilibrium data), i need to provide equi data:
For toggling selection and automatically updating equi data:
useEffect(() => {
if (vol === 'datapts') addEqui([{x: 0, y: 0}, {x: 1, y: 1}])
else if (vol === 'vol' && alpha > 1) equi_rel_from_alpha();
}, [vol, alpha]);
function equi_rel_from_alpha() {
try {
let newEqui = []
let x
let y_eq
for (let i = round(0); round(i) <= 1; i += 0.1) {
x = round(i)
y_eq = (alpha * x) / (1 + (alpha - 1) * x)
newEqui.push({ x: x, y: Number(y_eq.toFixed(4)) })
}
addEqui(newEqui)
} catch (err) {
setErr('Error in finding equilibrium curve from alpha')
}
}
function round(el) {
return Math.round(el * 100) / 100
}
Azeo is a random function as to calculate how many times the equi data lines will cross y=x line:
const [azeo, setAzeo] = useState([])
function check_azeo(){
const xy = {a: 1, b: -1, c: 0}
const len = equi.length
let azeo_temp = []
let pos = 1
if(len>2){
let p1
let p2
let ln
while(pos+1!=len-1){
p1 = equi[pos]
p2 = equi[pos+1]
if((xy.a*p1.x + xy.b*p1.y + xy.c) == 0){
azeo_temp.push(p1.x)
}
else if((xy.a*p1.x + xy.b*p1.y + xy.c)*(xy.a*p2.x + xy.b*p2.y + xy.c)<0){
ln = line_from_2points(p1, p2)
azeo_temp.push(intersection_of_2lines(xy, ln).x)
}
pos += 1
}
}
setAzeo(azeo_temp)
}
Now once I have selected the mode, there will be parameters provided (D, F, etc.) and mccabe_thiele function will be called based on criteria and mode as below:
useEffect(() => {
if (D && F && xD && xF && q && op_ratio && q && vol && alpha && equi.length > 1) {
if (D > 0 && F > 0 && F > D && xF < 1 && xD < 1 && xF > 0 && xD > 0 && alpha > 1 && F * xF > D * xD && xD > xF && op_ratio > 1) {
if (vol === 'vol') {
setErr('')
mcCabe_thiele()
}
else if (vol === 'datapts') {
if(azeo.length > 0){
setErr('Cant plot for azeotropes')
}
else if (equi.length >= 5) {
setErr('')
mcCabe_thiele()
}
else setErr('Atleast 5 equilibrium points must be provided')
}
}
else if (alpha <= 1) setErr('Relative volatility must be greater than 1')
else if (D <= 0) setErr('Distillate rate must be greater than 0')
else if (F <= 0) setErr('Feed rate must be greater than 0')
else if (F < D) setErr('Feed rate must be greater than distillate rate')
else if (F * xF < D * xD) setErr('You are expecting more than you are feeding')
else if (xD <= xF) setErr('Distillate purity must be more than feed purity')
else if (op_ratio <= 1) setErr('Optimum ratio must be greater than 1')
else if (xD <= 0) setErr('Distillate purity cannot be zero')
else if (xD >= 1) setErr('Distillate cannot be completely pure')
else if (xF <= 0) setErr('Feed cannot be single component')
else if (xF >= 1) setErr('Feed cannot be single component')
}
}, [alpha, D, F, xD, xF, q, op_ratio, equi, azeo])
Here, D, F, xD, xF, q, opt_ratio are parameters and need not be worried about.
I am facing a very very weird issue:
When I am using vol mode, its working completely fine but mccabe thiele is being called twice.
When I am switching to datapts mode and entering valid equi data (5 points), its working fine but again mccabe thiele is called twice.
In case when I am intentionally using points in datapts mode where azeo is non-empty array, weirdly still mccabe_thiele function is being called (which shouldnt be the case when azeo.length>0) and stages are being drawn according to mccabe thiele method (but using equi data of vol mode). So system is using vol mode equi data for calculation when azeo is non-empty, I want the plot to not execute the mccabe thiele function in this case.
I know the problem is too long, but I want the solution. I am not using any strict mode, please help!!! (all parameter inputs are working fine)
McCabe_math.jsx
import { useEffect } from "react"
export default function McCabe_math({ D, F, xF, xD, q, vol, alpha, op_ratio, setW, setxW, addEqui, setR, setErr, equi, setSteps, setfdInter, setRm, setStages, azeo, setAzeo }) {
function round(el) {
return Math.round(el * 100) / 100
}
useEffect(() => {
setErr('')
}, [vol, D, F, xD, xF, q, op_ratio])
useEffect(() => {
if (vol === 'datapts') addEqui([{x: 0, y: 0}, {x: 1, y: 1}])
else if (vol === 'vol' && alpha > 1) equi_rel_from_alpha();
}, [vol, alpha]);
// useEffect(() => {
// if (azeo.length > 0) setErr('Azeotrope formation')
// }, [azeo])
useEffect(() => {
if (D && F && xD && xF && q && op_ratio && q && vol && alpha && equi.length > 1) {
if (D > 0 && F > 0 && F > D && xF < 1 && xD < 1 && xF > 0 && xD > 0 && alpha > 1 && F * xF > D * xD && xD > xF && op_ratio > 1) {
if (vol === 'vol') {
setErr('')
mcCabe_thiele()
}
else if (vol === 'datapts') {
if(azeo.length > 0){
setErr('Cant plot for azeotropes')
setStages([])
}
else if (equi.length >= 5) {
setErr('')
mcCabe_thiele()
}
else setErr('Atleast 5 equilibrium points must be provided')
}
}
else if (alpha <= 1) setErr('Relative volatility must be greater than 1')
else if (D <= 0) setErr('Distillate rate must be greater than 0')
else if (F <= 0) setErr('Feed rate must be greater than 0')
else if (F < D) setErr('Feed rate must be greater than distillate rate')
else if (F * xF < D * xD) setErr('You are expecting more than you are feeding')
else if (xD <= xF) setErr('Distillate purity must be more than feed purity')
else if (op_ratio <= 1) setErr('Optimum ratio must be greater than 1')
else if (xD <= 0) setErr('Distillate purity cannot be zero')
else if (xD >= 1) setErr('Distillate cannot be completely pure')
else if (xF <= 0) setErr('Feed cannot be single component')
else if (xF >= 1) setErr('Feed cannot be single component')
}
}, [alpha, D, F, xD, xF, q, op_ratio, equi, azeo])
function equi_rel_from_alpha() {
try {
let newEqui = []
let x
let y_eq
for (let i = round(0); round(i) <= 1; i += 0.1) {
x = round(i)
y_eq = (alpha * x) / (1 + (alpha - 1) * x)
newEqui.push({ x: x, y: Number(y_eq.toFixed(4)) })
}
addEqui(newEqui)
} catch (err) {
setErr('Error in finding equilibrium curve from alpha')
}
}
function line_from_2points(p1, p2) {
let x1 = p1.x
let y1 = p1.y
let x2 = p2.x
let y2 = p2.y
let m = (y2 - y1) / (x2 - x1)
return { a: m, b: -1, c: y1 - m * x1 }
}
function closestPoint(a, b, c) {
let init = Math.abs(c / (Math.pow(a * a + b * b, 0.5)))
let min = { val: init, pt: { x: 0, y: 0 } }
for (let el of equi) {
let xi = el.x
let yi = el.y
let dist = Math.abs((a * xi + b * yi + c) / (Math.pow(a * a + b * b, 0.5)))
if (dist == 0) return { x: xi, y: yi, ptr: 'exact', dist: 0 } //when the intersection is an actual data point
else if (dist < min.val) {
min.val = dist
min.pt = { x: xi, y: yi }
}
}
return { x: min.pt.x, y: min.pt.y, ptr: 'non-exact', dist: min.val }
}
function sideofPoint(xi, yi, a, b, c) {
let s_origin = a * 0 + b * 0 + c
let s_point = a * xi + b * yi + c
if (s_origin * s_point < 0) return 'right'
else if (s_origin * s_point > 0) return 'left'
}
function complementaryPoint(xi, side) {
const index = equi.findIndex(el => el.x === xi)
if (side === 'right') return equi[index - 1]
else if (side === 'left') return equi[index + 1]
}
function intersection_of_2lines(l1, l2) {
let a1 = l1.a
let b1 = l1.b
let c1 = l1.c
let a2 = l2.a
let b2 = l2.b
let c2 = l2.c
return { x: (b1 * c2 - b2 * c1) / (a1 * b2 - a2 * b1), y: (a2 * c1 - a1 * c2) / (a1 * b2 - a2 * b1) }
}
function q_line() {
try {
if (Number(q) === 1) return { a: 1, b: 0, c: Number(-xF) }
else {
let feed_slope = (-1) * (q / (1 - q))
return { a: (-1) * feed_slope, b: 1, c: xF * (feed_slope - 1) }
}
} catch (err) {
setErr('Error in finding q line')
}
}
function feed_intersection() {
try {
let feed_slope
let closestPt
let side
let compPt
if (Number(q) === 1) {
//feed line eqn: x + 0*y - xF = 0
closestPt = closestPoint(1, 0, -1 * (xF))
if (closestPt.ptr === 'exact') return { x: closestPt.x, y: closestPt.y }
else {
side = sideofPoint(closestPt.x, closestPt.y, 1, 0, -1 * (xF))
}
}
else {
//feed line eqn: (-feed_slope)x + y + xF(feed_slope - 1) = 0
feed_slope = (-1) * (q / (1 - q))
closestPt = closestPoint((-1) * feed_slope, 1, xF * (feed_slope - 1), equi)
if (closestPt.ptr === 'exact') return { x: closestPt.x, y: closestPt.y }
else {
side = sideofPoint(closestPt.x, closestPt.y, (-1) * feed_slope, 1, xF * (feed_slope - 1))
}
}
compPt = complementaryPoint(closestPt.x, side)
let comp_line = line_from_2points(closestPt, compPt)
if (Number(q) === 1) return intersection_of_2lines(comp_line, { a: 1, b: 0, c: (-1) * Number(xF) })
else return intersection_of_2lines(comp_line, { a: (-1) * feed_slope, b: 1, c: xF * (feed_slope - 1) })
} catch (err) {
setErr('Error in finding feed intersection with curve')
}
}
function step_intersection(yi) {
let snap = -1;
for (let i = 0; i < equi.length; i++) {
if (equi[i].y === yi) return { x: equi[i].x, y: equi[i].y }; // Exact match
if (equi[i].y > yi) {
snap = i;
break; // Stop at the first point where y > yi
}
}
// If `yi` is outside the range, return null
if (snap === -1 || snap === 0) return null; // No valid intersection
let p1 = equi[snap - 1];
let p2 = equi[snap];
let l1 = line_from_2points(p1, p2);
let l2 = { a: 0, b: 1, c: -yi };
return intersection_of_2lines(l1, l2);
}
function mcCabe_thiele() {
console.log('mct called')
//top_composition
let top_compo = { x: xD, y: xD }
//to find out the intersection point of feed line with equilibrium curve after finding closest and complementary eq. data points and interpolation
const feed_equi = feed_intersection()
//q-line
let q_ln = q_line()
//pinch calculations
let pinch_slope = (feed_equi.y - top_compo.y) / (feed_equi.x - top_compo.x)
let Rmin = pinch_slope / (1 - pinch_slope)
setRm(Rmin)
let Ropt = Rmin * op_ratio
setR(Ropt)
let rect_slope = Ropt / (Ropt + 1)
let rect = { a: (-1) * rect_slope, b: 1, c: xD * (rect_slope - 1) }
//intersection of feed and rectifying line
let feed_rect_intersection = intersection_of_2lines(q_ln, rect)
try {
setfdInter(feed_rect_intersection)
} catch (err) {
setErr('Error in finding feed and rectification line intersection')
}
//stripping line
let W = F - D
setW(W)
let xW = (F * xF - D * xD) / W
setxW(xW)
let bottom_compo = { x: xW, y: xW }
let strip = line_from_2points(feed_rect_intersection, bottom_compo)
//steps calculation
let step_pts = []
let x_curr = xD
let y_curr
let stage = 'rect'
let check
let next
try {
while (true) {
//rectifying area
if (stage === 'rect') {
//horizontal step
y_curr = (-1) * (rect.a * x_curr + rect.c) / (rect.b)
//check if stripping zone is reached after last vertical dip
check = sideofPoint(x_curr, y_curr, q_ln.a, q_ln.b, q_ln.c)
if (check === 'left') {
stage = 'strip'
continue
}
step_pts.push({ x: x_curr, y: y_curr })
//if stripping zone is not reached
next = step_intersection(y_curr)
x_curr = next.x
y_curr = next.y
check = sideofPoint(x_curr, y_curr, q_ln.a, q_ln.b, q_ln.c)
if (check === 'left') {
step_pts.push({ x: x_curr, y: y_curr })
stage = 'strip'
continue
}
step_pts.push({ x: x_curr, y: y_curr })
//vertical step
next = intersection_of_2lines(rect, { a: 1, b: 0, c: -1 * x_curr })
x_curr = next.x
}
if (stage === 'strip') {
//horizontal step
y_curr = (-1) * (strip.a * x_curr + strip.c) / (strip.b)
if (x_curr < xW) {
step_pts.push({ x: x_curr, y: x_curr })
break
}
step_pts.push({ x: x_curr, y: y_curr })
next = step_intersection(y_curr, equi)
x_curr = next.x
y_curr = next.y
step_pts.push({ x: x_curr, y: y_curr })
//vertical step
next = intersection_of_2lines(strip, { a: 1, b: 0, c: -1 * x_curr })
x_curr = next.x
}
}
setSteps(step_pts)
setStages(() => Math.floor(step_pts.length / 2))
}
catch (err) {
console.log(err)
setErr('Error in step calculation')
}
}
return (
<>
</>
)
}
Mccabe_plot.jsx
import { Line } from 'react-chartjs-2';
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js';
import { useState, useEffect } from 'react';
export default function Mccabe_plot({ xW, xD, xF, fdInter, equi, steps, err, vol }) {
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const [x_eq, addx_eq] = useState([]);
const [y_eq, addy_eq] = useState([]);
const [x_steps, addx_steps] = useState([]);
const [y_steps, addy_steps] = useState([]);
const [chartData, setChartData] = useState(null);
useEffect(() => {
if (xD < 1 && xD>0 && xW > 0 && xW<1 && xF > 0 && xF < 1 && xD>xF && xF>xW) {
if((vol==='vol' && err === '')||(vol==='datapts')){
setChartData(equi.length>=5?{
datasets: [
{
label: 'x_eq vs y_eq',
data: equi.map((point) => ({
x: Number(point.x).toFixed(4),
y: Number(point.y).toFixed(4),
})),
fill: false,
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.1,
},
{
label: 'Steps',
data: steps.map((point) => ({
x: Number(point.x).toFixed(4),
y: Number(point.y).toFixed(4),
})),
fill: false,
borderColor: 'brown',
tension: 0.1,
},
{
label: 'Enriching Line',
data: [
{ x: xD, y: xD },
{ x: fdInter.x, y: fdInter.y },
],
fill: true,
borderColor: 'red',
tension: 0.1,
},
{
label: 'Stripping Line',
data: [
{ x: xW, y: xW },
{ x: fdInter.x, y: fdInter.y },
],
fill: true,
borderColor: 'blue',
tension: 0.1,
},
{
label: 'Feed Line',
data: [
{ x: xF, y: xF },
{ x: fdInter.x, y: fdInter.y },
],
fill: true,
borderColor: 'grey',
tension: 0.1,
},
{
label: 'x=y',
data: [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
],
fill: true,
borderColor: 'black',
tension: 0.1,
}
]
}:
{
datasets: [
{
label: 'x_eq vs y_eq',
data: equi.map((point) => ({
x: Number(point.x).toFixed(4),
y: Number(point.y).toFixed(4),
})),
fill: false,
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.1,
},
{
label: 'Enriching Line',
data: [
{ x: xD, y: xD },
{ x: fdInter.x, y: fdInter.y },
],
fill: true,
borderColor: 'red',
tension: 0.1,
},
{
label: 'Stripping Line',
data: [
{ x: xW, y: xW },
{ x: fdInter.x, y: fdInter.y },
],
fill: true,
borderColor: 'blue',
tension: 0.1,
},
{
label: 'Feed Line',
data: [
{ x: xF, y: xF },
{ x: fdInter.x, y: fdInter.y },
],
fill: true,
borderColor: 'grey',
tension: 0.1,
},
{
label: 'x=y',
data: [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
],
fill: true,
borderColor: 'black',
tension: 0.1,
}
]
});
}
}
}, [equi, steps, err, xD, xW, xF, vol]);
const options = {
responsive: true,
scales: {
x: {
type: 'linear',
position: 'bottom',
title: {
display: true,
text: 'X',
font: {
size: 15,
weight: 'normal',
},
grid: {
borderColor: 'black',
borderWidth: 5,
},
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Y',
font: {
size: 15,
weight: 'normal',
},
},
},
},
};
return (
<div>
{chartData ? (
<Line data={chartData} options={options} width={600} height={570} />
) : (
<p>Invalid input values. Chart not updated.</p>
)}
</div>
);
}