function renderNeutralInputs() {
// Dynamically render neutral position inputs based on neutralCount
const container = document.getElementById("neutralInputs");
if (!container) return;
const countEl = document.getElementById("neutralCount");
const n = Math.max(1, parseInt(countEl && countEl.value ? countEl.value : 1, 10));
let html = "";
for (let i = 1; i <= n; i++) {
const defaultX = i === 1 ? 10 : (i === 2 ? 40 : 10 + (i - 1) * 15);
const defaultY = 71;
html += `` +
`, ` +
` (ft)
`;
}
container.innerHTML = html;
setWarning("");
}
function setWarning(msg) {
const el = document.getElementById("warning");
if (el) {
el.textContent = msg || "";
el.style.display = msg ? "block" : "none";
} else if (msg) {
// Fallback alert if warning container is missing
alert(msg);
}
}
function calcImpedance() {
const m_to_ft = (m) => m * 3.28084;
const ft_to_m = (ft) => ft * 0.30479999;
const mi_to_m = (mi) => mi * 1.60934 * 1000;
const km_to_mi = (km) => km * 0.621371;
const dist = (x1, y1, x2, y2) => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
// System parameters
const f = parseFloat(document.getElementById("frequency").value); // Hz
// Conductor positions (feet)
//const apos = [parseFloat(document.getElementById("ax".value)), parseFloat(document.getElementById("ay").value)];
const apos = [parseFloat(document.getElementById("ax").value), parseFloat(document.getElementById("ay").value)];
const bpos = [parseFloat(document.getElementById("bx").value), parseFloat(document.getElementById("by").value)];
const cpos = [parseFloat(document.getElementById("cx").value), parseFloat(document.getElementById("cy").value)];
// Neutral positions (feet) - arbitrary count
const neutralCount = Math.max(1, parseInt(document.getElementById("neutralCount").value || "1", 10));
const npos = [];
for (let i = 1; i <= neutralCount; i++) {
const xi = parseFloat(document.getElementById(`n${i}x`).value);
const yi = parseFloat(document.getElementById(`n${i}y`).value);
npos.push([xi, yi]);
}
// Validate no duplicate neutral positions (avoid log(0) singularities)
const eps = 1e-9; // feet tolerance
let dupMsg = "";
for (let i = 0; i < neutralCount; i++) {
for (let j = i + 1; j < neutralCount; j++) {
const d = dist(npos[i][0], npos[i][1], npos[j][0], npos[j][1]);
if (!isFinite(d) || isNaN(d)) { continue; }
if (d < eps) {
dupMsg = `Duplicate neutral positions detected: Neutral ${i+1} and Neutral ${j+1} share the same (x, y). Please adjust their positions.`;
break;
}
}
if (dupMsg) break;
}
if (dupMsg) {
setWarning(dupMsg);
return; // Abort calculation
} else {
setWarning("");
}
// Conductor properties
const Ra = parseFloat(document.getElementById("Ra").value); // Ohms per mile per conductor
const Rb = Ra;
const Rc = Ra;
const GMRa = parseFloat(document.getElementById("GMRa").value); // Geometric mean radius (feet)
const GMRb = GMRa;
const GMRc = GMRa;
const ODa = parseFloat(document.getElementById("ODa").value);
const bundle = parseFloat(document.getElementById("bundle").value); // Conductors in bundle
const bundle_spacing = parseFloat(document.getElementById("bundle_spacing").value); // feet
// Neutral conductor properties
const Rn1 = parseFloat(document.getElementById("Rn1").value); // Ohms per km per neutral conductor (assumed identical)
const GMRn1 = parseFloat(document.getElementById("GMRn1").value); // feet
const ODn = parseFloat(document.getElementById("ODn").value);
// values for dry earth
let Dkkp = 1;
if(document.getElementById("material").value == "seawater") {
Dkkp = m_to_ft(38.25); // feet
} else if (document.getElementById("material").value == "swampyground") {
Dkkp = m_to_ft(560);
} else if (document.getElementById("material").value == "dampearth") {
Dkkp = m_to_ft(850);
} else if (document.getElementById("material").value == "dryearth") {
Dkkp = m_to_ft(2690);
} else if (document.getElementById("material").value == "pureslate") {
Dkkp = m_to_ft(269000);
} else if (document.getElementById("material").value == "sandstone") {
Dkkp = m_to_ft(2690000);
} else {
Dkkp = m_to_ft(658.5*Math.pow((parseFloat(document.getElementById("resistivity").value)/f),0.5));
}
const Rkp = 9.869E-7 * f;
const Rk = (Ra / mi_to_m(1)) / bundle; // convert to ohms per meter per phase
const Rn = (Rn1 / 1000); // convert to ohms per meter per neutral
let Dkkc = Math.pow(GMRa * Math.pow(bundle_spacing, (bundle - 1)), (1 / bundle));
if (bundle >= 4){
Dkkc = Dkkc * 1.091;
}
const Dkkg = GMRn1;
const jwmu = math.multiply(math.complex(0,1), (2*Math.PI*f)*(2E-7));
const Zaa = math.add(Rk + Rkp, math.multiply(jwmu, (math.log(Dkkp / Dkkc))));
const Zbb = Zaa;
const Zcc = Zaa;
const Zab = math.add(Rkp, math.multiply(jwmu, (math.log(dist(apos[0], apos[1], bpos[0], bpos[1] - Dkkp) / dist(apos[0], apos[1], bpos[0], bpos[1])))));
const Zbc = math.add(Rkp, math.multiply(jwmu, (math.log(dist(bpos[0], bpos[1], cpos[0], cpos[1] - Dkkp) / dist(bpos[0], bpos[1], cpos[0], cpos[1])))));
const Zac = math.add(Rkp, math.multiply(jwmu, (math.log(dist(apos[0], apos[1], cpos[0], cpos[1] - Dkkp) / dist(apos[0], apos[1], cpos[0], cpos[1])))));
// Build Zb (3 x N) and Zd (N x N)
const Zb = [[], [], []];
for (let k = 0; k < neutralCount; k++) {
const nk = npos[k];
const Za_n = math.add(Rkp, math.multiply(jwmu, (math.log(dist(apos[0], apos[1], nk[0], nk[1] - Dkkp) / dist(apos[0], apos[1], nk[0], nk[1])))));
const Zb_n = math.add(Rkp, math.multiply(jwmu, (math.log(dist(bpos[0], bpos[1], nk[0], nk[1] - Dkkp) / dist(bpos[0], bpos[1], nk[0], nk[1])))));
const Zc_n = math.add(Rkp, math.multiply(jwmu, (math.log(dist(cpos[0], cpos[1], nk[0], nk[1] - Dkkp) / dist(cpos[0], cpos[1], nk[0], nk[1])))));
Zb[0][k] = Za_n;
Zb[1][k] = Zb_n;
Zb[2][k] = Zc_n;
}
const Zd = new Array(neutralCount).fill(null).map(()=> new Array(neutralCount).fill(null));
for (let i = 0; i < neutralCount; i++) {
for (let j = 0; j < neutralCount; j++) {
if (i === j) {
Zd[i][j] = math.add(Rn + Rkp, math.multiply(jwmu, (math.log(Dkkp / GMRn1))));
} else {
Zd[i][j] = math.add(Rkp, math.multiply(jwmu, (math.log(dist(npos[i][0], npos[i][1], npos[j][0], npos[j][1] - Dkkp) / dist(npos[i][0], npos[i][1], npos[j][0], npos[j][1])))));
}
}
}
const Zc = math.transpose(Zb);
const Za = [
[Zaa, Zab, Zac],
[Zab, Zbb, Zbc],
[Zac, Zbc, Zcc]
];
const Zp = math.subtract(Za, math.multiply(math.multiply(Zb, math.inv(Zd)), Zc));
const Zs = math.divide(math.add( math.add(Zp[0][0], Zp[1][1]), Zp[2][2]),3);
const Zm = math.divide(math.add( math.add(Zp[0][1], Zp[0][2]), Zp[1][2]),3);
console.log(Zs);
console.log(Zm);
const Zpsym = [
[Zs, Zm, Zm],
[Zm, Zs, Zm],
[Zm, Zm, Zs]
];
const a = math.complex(math.cos(120 * (Math.PI / 180)), math.sin(120 * (Math.PI / 180)));
const Ainv = [
[1, 1, 1],
[1, a, math.pow(a, 2)],
[1, math.pow(a, 2), a]
];
const A = [
[1, 1, 1],
[1, math.pow(a, 2), a],
[1, a, math.pow(a, 2)]
];
const Zshat = math.multiply((1 / 3), math.multiply(math.multiply(Ainv, Zpsym), A)); // ohms per meter
console.log(`Z0 = ${Zshat[0][0].re * 1000} + j${Zshat[0][0].im * 1000} ohm/km (zero sequence)`);
console.log(`Z1 = ${Zshat[1][1].re * 1000} + j${Zshat[1][1].im * 1000} ohm/km (positive sequence)`);
console.log(`Z2 = ${Zshat[2][2].re * 1000} + j${Zshat[2][2].im * 1000} ohm/km (negative sequence)`);
document.getElementById("Z0").textContent = `${Zshat[0][0].re * 1000} + j${Zshat[0][0].im * 1000} ohm/km (zero sequence)`;
document.getElementById("Z1").textContent = `${Zshat[1][1].re * 1000} + j${Zshat[1][1].im * 1000} ohm/km (positive sequence)`;
document.getElementById("Z2").textContent = `${Zshat[2][2].re * 1000} + j${Zshat[2][2].im * 1000} ohm/km (negative sequence)`;
const ra = ODa / (2 * 12); // inches to feet
const rn = ODn / (2 * 12);
let Dsc = Math.pow(ra * Math.pow(bundle_spacing, (bundle - 1)), (1 / bundle));
if (bundle >= 4) {
Dsc *= 1.091;
}
let Dscn = rn;
const Paa = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((apos[1] * 2) / Dsc);
const Pbb = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((bpos[1] * 2) / Dsc);
const Pcc = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((cpos[1] * 2) / Dsc);
const Pab = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((dist(apos[0], apos[1], bpos[0], (-1 * bpos[1]))) / (dist(apos[0], apos[1], bpos[0], bpos[1])));
const Pac = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((dist(apos[0], apos[1], cpos[0], (-1 * cpos[1]))) / (dist(apos[0], apos[1], cpos[0], cpos[1])));
const Pbc = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((dist(bpos[0], bpos[1], cpos[0], (-1 * cpos[1]))) / (dist(bpos[0], bpos[1], cpos[0], cpos[1])));
// Build Pb (3 x N), Pc (N x 3), Pd (N x N)
const Pb = [[], [], []];
for (let k = 0; k < neutralCount; k++) {
const nk = npos[k];
const Pan = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((dist(apos[0], apos[1], nk[0], (-1 * nk[1]))) / (dist(apos[0], apos[1], nk[0], nk[1])));
const Pbn = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((dist(bpos[0], bpos[1], nk[0], (-1 * nk[1]))) / (dist(bpos[0], bpos[1], nk[0], nk[1])));
const Pcn = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((dist(cpos[0], cpos[1], nk[0], (-1 * nk[1]))) / (dist(cpos[0], cpos[1], nk[0], nk[1])));
Pb[0][k] = Pan;
Pb[1][k] = Pbn;
Pb[2][k] = Pcn;
}
const Pd = new Array(neutralCount).fill(null).map(()=> new Array(neutralCount).fill(null));
for (let i = 0; i < neutralCount; i++) {
for (let j = 0; j < neutralCount; j++) {
if (i === j) {
Pd[i][j] = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((npos[i][1] * 2) / Dscn);
} else {
Pd[i][j] = (1 / (2 * Math.PI * 8.854E-12)) * Math.log((dist(npos[i][0], npos[i][1], npos[j][0], (-1 * npos[j][1]))) / (dist(npos[i][0], npos[i][1], npos[j][0], npos[j][1])));
}
}
}
const Pc = math.transpose(Pb);
const Pa = [
[Paa, Pab, Pac],
[Pab, Pbb, Pbc],
[Pac, Pbc, Pcc]
];
const Cp = math.inv(math.subtract(Pa, math.multiply(math.multiply(Pb, math.inv(Pd)), Pc)));
const Caa = math.divide(math.add( math.add(Cp[0][0], Cp[1][1]), Cp[2][2]),3);
const Cab = math.divide(math.add( math.add(Cp[0][1], Cp[0][2]), Cp[1][2]),3);
const Cpsym = [
[Caa, Cab, Cab],
[Cab, Caa, Cab],
[Cab, Cab, Caa]
];
//Cphat = np.multiply(1j*(2*np.pi*f)*1000,(1/3)*np.matmul(np.matmul(Ainv,Cpsym),A))
// Cshat = (1/3)*np.matmul(np.matmul(Ainv,Cpsym),A) //ohms per meter
// 1j*2*np.pi*Cpsym
const Cphat = math.multiply(math.multiply(math.complex(0,1), (2*Math.PI*f/3)), math.multiply(math.multiply(Ainv, Cpsym), A));
console.log(Cphat);
console.log(`Y0 = ${Cphat[0][0].im * 1000} ohm/km (zero sequence)`);
console.log(`Y1 = ${Cphat[1][1].im * 1000} ohm/km (positive sequence)`);
console.log(`Y2 = ${Cphat[2][2].im * 1000} ohm/km (negative sequence)`);
document.getElementById("Y0").textContent = `j${Cphat[0][0].im * 1000} ohm/km (zero sequence)`;
document.getElementById("Y1").textContent = `j${Cphat[1][1].im * 1000} ohm/km (positive sequence)`;
document.getElementById("Y2").textContent = `j${Cphat[2][2].im * 1000} ohm/km (negative sequence)`;
}
function dropdownChangeHandler() {
if(document.getElementById("material").value=="custom") {
document.getElementById("resistivitylabel").style="display:inline;";
document.getElementById("resistivity").style="display:inline;";
document.getElementById("resistivityunits").style="display:inline;";
} else {
document.getElementById("resistivitylabel").style="display:none;";
document.getElementById("resistivity").style="display:none;";
document.getElementById("resistivityunits").style="display:none;";
}
}
// Initialize dynamic fields on load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', renderNeutralInputs);
} else {
renderNeutralInputs();
}