kopia lustrzana https://github.com/miguelvaca/vk3cpu
927 wiersze
33 KiB
HTML
927 wiersze
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<!-- @file : antenna.html -->
|
|
<!-- @author : J Miguel Vaca -->
|
|
<!-- @remark : This webpage uses Computational Electromagnetics (CEM) to solve antenna currents and fields. It then uses WebGL to visualise the currents -->
|
|
<!-- : and fields produced by the antenna. -->
|
|
<!-- : Idea - allow adding wire antennas, one-at-a-time. Then have controls at the ends to move ends, stretch or shring the wire. -->
|
|
<!-- Have a control in the middle that can be used to reposition the camera. Also a control to move the feedpoint along the wire. -->
|
|
<!-- Support multiple feedpoints, but with a single master feed (or frequency), and the others are slaved off the master, but with a controllable phase -->
|
|
<!-- shift that will allow modelling of phased-arrays. -->
|
|
<head>
|
|
<title>VK3CPU Antenna Simulator</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
|
<link type="text/css" href="css/base.css" rel="stylesheet"/>
|
|
<link type="text/css" href="css/visualisation.css" rel="stylesheet"/>
|
|
</head>
|
|
<body>
|
|
<!--div id="info">
|
|
Visualisation by : <a href="mailto:vacamiguel@gmail.com">J Miguel Vaca</a>
|
|
</div-->
|
|
|
|
<!-- math.js library scripts -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/7.5.1/math.min.js"></script>
|
|
<script src="./dat.gui.min.js"></script>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://threejs.org/build/three.module.js",
|
|
"three/addons/": "https://threejs.org/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="module">
|
|
|
|
import * as THREE from 'three';
|
|
|
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
|
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
// Heatmap vertex shader - Use this to update the position due to camera projection
|
|
var radiationPatternVertexShader =
|
|
"uniform vec4 hiColor; \n" +
|
|
"uniform vec4 loColor; \n" +
|
|
"uniform float maxHistogramValue; \n" +
|
|
"uniform float contrast; \n" +
|
|
"uniform float shape; \n" +
|
|
"varying vec4 vColor; \n" +
|
|
"\n" +
|
|
"void main() { \n" +
|
|
" vec4 mvPosition; \n" +
|
|
" // UV.x contains histogram value \n" +
|
|
" // UV.y is identical to UV.x \n" +
|
|
" if(maxHistogramValue > 0.0) { \n" +
|
|
" vColor = pow((uv.x / maxHistogramValue), contrast) * (hiColor-loColor) + loColor; \n" +
|
|
" mvPosition = modelViewMatrix * vec4( position.xyz*pow((uv.x / maxHistogramValue),shape), 1.0 ); \n" +
|
|
" } else { \n" +
|
|
" vColor = hiColor; \n" +
|
|
" mvPosition = modelViewMatrix * vec4( position.xyz, 1.0 ); \n" +
|
|
" } \n" +
|
|
" \n" +
|
|
" gl_PointSize = 2.0; \n" +
|
|
" gl_Position = projectionMatrix * mvPosition; \n" +
|
|
"} \n";
|
|
|
|
var container; //, stats;
|
|
var camera, scene, renderer, geometry, controls;
|
|
var clock = new THREE.Clock();
|
|
var tick = 0;
|
|
|
|
var ant = 0;
|
|
var current_antenna_object = 0;
|
|
var ground_plane = 0;
|
|
var height_above_ground = 0;
|
|
|
|
// Recalculate all variables. Triggered whenever the system parameter inputs are changed:
|
|
function recalculate() {
|
|
// Read all inputs and store values in wavelengths:
|
|
|
|
// Create segments of all the wires with appropriate lengths:
|
|
|
|
// Create 3D representation and reload the scene:
|
|
}
|
|
|
|
class AntennaTypes {
|
|
//
|
|
constructor() {
|
|
this.frequency = 3e8;
|
|
|
|
this.antenna_types = {
|
|
'order' : ['Horizontal Dipole', 'Vertical Dipole', 'Vertical Monopole', 'Inverted Vee', 'Inverted L', 'Loop Large Triangle', 'Quad', 'H Yagi 5-element', 'Spiderbeam 5'],
|
|
'antennas' : {
|
|
'Vertical Dipole' : {
|
|
'vertex' : [
|
|
[0.00, -0.25, 0.00], [0.00, 0.25, 0.00]
|
|
],
|
|
'wires' : [
|
|
[0, 1]
|
|
],
|
|
},
|
|
'Horizontal Dipole' : {
|
|
'vertex' : [
|
|
[-0.25, 0.00, 0.00], [0.25, 0.00, 0.00]
|
|
],
|
|
'wires' : [
|
|
[0, 1]
|
|
],
|
|
},
|
|
'Vertical Monopole' : {
|
|
'vertex' : [
|
|
[0.00, 0.05, 0.00], [0.00, 0.55, 0.00]
|
|
],
|
|
'wires' : [
|
|
[0, 1]
|
|
],
|
|
},
|
|
'Inverted Vee' : {
|
|
'vertex' : [
|
|
[-0.25, 0.00, 0.00], [0.00, 0.25, 0.00], [0.25, 0.00, 0.00]
|
|
],
|
|
'wires' : [
|
|
[0, 1, 2]
|
|
],
|
|
},
|
|
'Inverted L' : {
|
|
'vertex' : [
|
|
[0.00, 0.00, 0.00], [0.00, 0.35, 0.00], [0.00, 0.35, 0.20]
|
|
],
|
|
'wires' : [
|
|
[0, 1, 2]
|
|
],
|
|
},
|
|
'Loop Large Triangle' : {
|
|
'vertex' : [
|
|
[-0.05, 0.00, 0.00], [-0.35, 0.00, 0.00], [0.00, 0.35, 0.00], [0.35, 0.00, 0.00], [0.05, 0.00, 0.00]
|
|
],
|
|
'wires' : [
|
|
[0, 1, 2, 3, 4]
|
|
],
|
|
},
|
|
'Quad' : {
|
|
'vertex' : [
|
|
[-0.05, 0.00, 0.00], [-0.35, 0.00, 0.00], [-0.35, 0.35, 0.00], [0.35, 0.35, 0.00], [0.35, 0.00, 0.00], [0.05, 0.00, 0.00]
|
|
],
|
|
'wires' : [
|
|
[0, 1, 2, 3, 4, 5]
|
|
],
|
|
},
|
|
'H Yagi 5-element' : {
|
|
'vertex' : [
|
|
[-0.35, 0.00, -0.25], [0.35, 0.00, -0.25], // Reflector
|
|
[-0.25, 0.00, 0.00], [0.25, 0.00, 0.00], // Exciter
|
|
[-0.25, 0.00, 0.25], [0.25, 0.00, 0.25], // Director
|
|
[-0.25, 0.00, 0.50], [0.25, 0.00, 0.50], // Director
|
|
[-0.25, 0.00, 0.75], [0.25, 0.00, 0.75] // Director
|
|
],
|
|
'wires' : [
|
|
[0, 1], // Reflector
|
|
[2, 3], // Exciter
|
|
[4, 5], // Director
|
|
[6, 7], // Director
|
|
[8, 9] // Director
|
|
],
|
|
},
|
|
'Spiderbeam 5' : {
|
|
'vertex' : [
|
|
[-0.25, 0.00, -0.35], [-0.25, 0.00, 0.35], // Reflector
|
|
[0.00, 0.00, -0.25], [0.00, 0.00, 0.25], // Exciter
|
|
[0.25, 0.00, -0.25], [0.25, 0.00, 0.25], // Director
|
|
[0.50, 0.00, -0.25], [0.50, 0.00, 0.25], // Director
|
|
[0.75, 0.00, -0.25], [0.75, 0.00, 0.25]
|
|
],
|
|
'wires' : [
|
|
[0, 1], // Reflector
|
|
[2, 3], // Exciter
|
|
[4, 5], // Director
|
|
[6, 7], // Director
|
|
[8, 9] // Director
|
|
],
|
|
}
|
|
},
|
|
};
|
|
this.current_type = this.antenna_types['order'][3];
|
|
//console.log(this.current_type);
|
|
}
|
|
|
|
solve() {
|
|
// Fill the impedance Z matrix:
|
|
// Invert Z to produce admittance Y matrix:
|
|
// Create the V array:
|
|
// I = YV:
|
|
// Store (complex) input impedance:
|
|
// Solve radiation pattern:
|
|
}
|
|
|
|
setAntennaType(antenna_type) {
|
|
//console.log("setAntennaType" + antenna_type);
|
|
this.current_type = antenna_type;
|
|
}
|
|
|
|
// Return the wire elements for solving:
|
|
getWires() {
|
|
this.wires = [];
|
|
//
|
|
var ws = this.antenna_types['antennas'][this.current_type]['wires'];
|
|
var vs = this.antenna_types['antennas'][this.current_type]['vertex'];
|
|
|
|
ws.forEach(w => {
|
|
//
|
|
var ww = {};
|
|
// ww.length = ;
|
|
// ww.seg_len = ;
|
|
ww.radius = 0.0001;
|
|
ww.points = [];
|
|
for (let index = 0; index < (w.length-1); index++) {
|
|
const wire_length = Math.sqrt(
|
|
(vs[w[index]][0] - vs[w[index+1]][0])**2 +
|
|
(vs[w[index]][1] - vs[w[index+1]][1])**2 +
|
|
(vs[w[index]][2] - vs[w[index+1]][2])**2
|
|
);
|
|
// Minimum of 10 segments per half-wavelength => 0.05
|
|
const max_segment_length = 0.05;
|
|
const segments = Math.round(wire_length / max_segment_length);
|
|
ww.seg_len = wire_length / segments;
|
|
for (let ii = 0; ii < segments; ii++) {
|
|
var frac = 1.0 * ii / segments;
|
|
var x = vs[w[index]][0] * (1.0 - frac) + frac * vs[w[index+1]][0];
|
|
var y = vs[w[index]][1] * (1.0 - frac) + frac * vs[w[index+1]][1];
|
|
var z = vs[w[index]][2] * (1.0 - frac) + frac * vs[w[index+1]][2];
|
|
ww.points.push([x,y,z]);
|
|
|
|
frac = 1.0 * (ii + 0.5) / segments;
|
|
x = vs[w[index]][0] * (1.0 - frac) + frac * vs[w[index+1]][0];
|
|
y = vs[w[index]][1] * (1.0 - frac) + frac * vs[w[index+1]][1];
|
|
z = vs[w[index]][2] * (1.0 - frac) + frac * vs[w[index+1]][2];
|
|
ww.points.push([x,y,z]);
|
|
}
|
|
}
|
|
// Add the final point:
|
|
x = vs[w[w.length-1]][0];
|
|
y = vs[w[w.length-1]][1];
|
|
z = vs[w[w.length-1]][2];
|
|
ww.points.push([x,y,z]);
|
|
//retval.push(ww);
|
|
this.wires.push(ww);
|
|
});
|
|
return this.wires;
|
|
}
|
|
|
|
//
|
|
getThreeObject3D = function () {
|
|
const material = new THREE.LineBasicMaterial({ color: 0xffff00, linewidth: 1 });
|
|
const antenna_view = new THREE.Group();
|
|
//
|
|
const ww = this.antenna_types['antennas'][this.current_type]['wires'];
|
|
const vv = this.antenna_types['antennas'][this.current_type]['vertex'];
|
|
ww.forEach(wire => {
|
|
//console.log(wire);
|
|
var vertices = new Float32Array(wire.length * 3);
|
|
var vidx = 0;
|
|
const scale_factor = 200.0; // Roughly pixels per wavelength
|
|
// Copy the vertex locations across into a Float32Array for the geometry:
|
|
wire.forEach((vertex) => {
|
|
//console.log(vertex, vv[vertex]);
|
|
vertices[vidx++] = (vv[vertex][0] * scale_factor);
|
|
vertices[vidx++] = (vv[vertex][1] * scale_factor);
|
|
vertices[vidx++] = (vv[vertex][2] * scale_factor);
|
|
});
|
|
//
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
|
// create a new wire with
|
|
const wire_line = new THREE.Line(geometry, material);
|
|
antenna_view.add(wire_line);
|
|
});
|
|
// Add the antenna into the scene:
|
|
return antenna_view;
|
|
};
|
|
}
|
|
|
|
class WireAntennaModel {
|
|
// constructor:
|
|
constructor(ws, vs) {
|
|
// Object variables:
|
|
this.wires = [];
|
|
this.Z = [];
|
|
this.Y = [];
|
|
|
|
}
|
|
|
|
//
|
|
psi(seg_n, seg_m, point_n, point_m) {
|
|
var retval = 0.0;
|
|
const k = 2.0 * Math.PI; // Normalised wavelength is equal to 1.0 - otherwise 2*pi/wavelength
|
|
const fourPI = 4.0 * Math.PI;
|
|
var Rmn = 0.0;
|
|
// From MININEC thesis (3-36) and (3-37):
|
|
if(point_n==point_m) {
|
|
//console.log(point_n, point_m);
|
|
retval = math.complex((1.0/(2.0*Math.PI*seg_n.len)) * Math.log(seg_n.len / seg_n.radius), (-k/fourPI));
|
|
//console.log(retval, seg_n.len, seg_n.radius);
|
|
} else {
|
|
Rmn = Math.sqrt((point_m[0] - point_n[0])**2 + (point_m[1] - point_n[1])**2 + (point_m[2] - point_n[2])**2);
|
|
retval = math.multiply(math.complex(Math.cos(k * Rmn), -Math.sin(k * Rmn)), (1/(fourPI*Rmn)));
|
|
}
|
|
//console.log(n, m, retval);
|
|
return retval;
|
|
}
|
|
/*
|
|
distanceBetween(p1, p2) {
|
|
// p1 : [x1,y1,z1]; p2 : [x2,y2,z2]
|
|
var distance_in_lambda = 0.0;
|
|
distance_in_lambda = Math.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2 + (p2[2] - p1[2])**2);
|
|
return distance_in_lambda;
|
|
}
|
|
*/
|
|
calculateZMatrix() {
|
|
const w = 2.0 * Math.PI * this.frequency;
|
|
const k = 2.0 * Math.PI * this.frequency / 3e8; // 2*pi/lambda
|
|
const e0 = 8.854187e-12;
|
|
const mu0 = 4.0 * Math.PI * 1e-7;
|
|
const fourPI = 4.0 * Math.PI;
|
|
|
|
var segments = [];
|
|
this.wires.forEach(wire => {
|
|
// This assumes the length MUST always be a positive ODD number:
|
|
for (let m = 1; m < (wire.points.length-1); m+=2) {
|
|
//
|
|
var t_seg = {};
|
|
t_seg.start = wire.points[m-1];
|
|
t_seg.end = wire.points[m+1];
|
|
t_seg.mid = wire.points[m];
|
|
t_seg.len = wire.seg_len;
|
|
t_seg.radius = wire.radius;
|
|
t_seg.feedpoint = false;
|
|
|
|
segments.push(t_seg);
|
|
}
|
|
});
|
|
console.log(segments);
|
|
|
|
this.Z = [];
|
|
for (let m = 0; m < segments.length; m++) {
|
|
var row = [];
|
|
for (let n = 0; n < segments.length; n++) {
|
|
// Use Harrington's method:
|
|
//console.log(m, n);
|
|
var tmp = math.dot(math.subtract(segments[n].end, segments[n].start), math.subtract(segments[m].end, segments[m].start));
|
|
//var tmp = math.dot(math.subtract(wire.points[n+1], wire.points[n-1]), math.subtract(wire.points[m+1], wire.points[m-1]));
|
|
tmp *= w * mu0;
|
|
tmp = math.multiply(math.complex(0,tmp), this.psi(segments[n], segments[m], segments[n].mid, segments[m].mid));
|
|
// tmp = math.multiply(math.complex(0,tmp), this.psi(wire, n, m));
|
|
var tmp2 = math.add(this.psi(segments[n], segments[m], segments[n].end, segments[m].end), this.psi(segments[n], segments[m], segments[n].start, segments[m].start));
|
|
// var tmp2 = math.add(this.psi(wire, n+1, m+1), this.psi(wire, n-1, m-1));
|
|
var tmp3 = math.add(this.psi(segments[n], segments[m], segments[n].start, segments[m].end), this.psi(segments[n], segments[m], segments[n].end, segments[m].start));
|
|
//var tmp3 = math.add(this.psi(wire, n-1, m+1), this.psi(wire, n+1, m-1));
|
|
var tmp4 = math.subtract(tmp2, tmp3);
|
|
tmp2 = math.multiply(tmp4, math.complex(0,-1/(w*e0)));
|
|
row.push(math.add(tmp, tmp2));
|
|
}
|
|
this.Z.push(row);
|
|
}
|
|
|
|
//console.log(this.Z);
|
|
this.Y = math.inv(this.Z);
|
|
return this.Z;
|
|
}
|
|
|
|
createVoltageVector() {
|
|
this.V = [];
|
|
for(var i=0; i<this.Z.length; i++){
|
|
if(i == Math.round(this.Z.length/2)) {
|
|
this.V.push(math.complex(1,0));
|
|
} else {
|
|
this.V.push(math.complex(0,0));
|
|
}
|
|
}
|
|
return this.V;
|
|
}
|
|
|
|
calculateVoltage() {
|
|
var retval = [];
|
|
var x_axis = 0.0;
|
|
for(var i=0; i<V.length; i++) {
|
|
x_axis += wire.seg_len;
|
|
retval.push({x:x_axis, y:V[i].toPolar().r});
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
calculateCurrent() {
|
|
var retval = [];
|
|
var x_axis = 0.0;
|
|
for(var i=0; i<I.length; i++) {
|
|
x_axis += wire.seg_len;
|
|
retval.push({x:x_axis, y:I[i].toPolar().r});
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
distanceToOrigin(px) {
|
|
return(Math.sqrt(px[0]**2 + px[1]**2 + px[2]**2));
|
|
}
|
|
|
|
/*getPatternAt(el, az) {*/
|
|
getPatternAt(x, y, z) {
|
|
// Return the magnitude of the radiation pattern in the xyz direction. Later, update this to provide a composite object containing polarization-specific patterns:
|
|
|
|
// First, refer all the current segments to the origin, along the line subtending along the el-az direction:
|
|
this.wires.forEach(wire => {
|
|
wire.forEach(segment => {
|
|
// Calculate the dot-product between the observation vector, to the origin-midpoint vector:
|
|
var phase_distance = math.dot(segment.mid, );
|
|
|
|
// Use each segment's midpoint for the xyz value, relative to x0y0z0:
|
|
var distance = distanceToOrigin(segment.mid);
|
|
|
|
// Actually, we need the distance to the plane perpendicular to the observation point, that goes through the origin:
|
|
|
|
});
|
|
});
|
|
|
|
// Next, return the current components in the vertical and horizontal direction:
|
|
|
|
return 1.0;
|
|
}
|
|
|
|
}
|
|
|
|
class WireAntennaView {
|
|
// constructor:
|
|
constructor() {
|
|
//
|
|
}
|
|
|
|
loadAntenna(wire_segments) {
|
|
//
|
|
}
|
|
|
|
//
|
|
getThreeObject3D = function () {
|
|
const material = new THREE.LineBasicMaterial({ color: 0xffff00, linewidth: 1 });
|
|
const antenna_view = new THREE.Group();
|
|
//
|
|
const ww = this.antenna_types['antennas'][this.current_type]['wires'];
|
|
const vv = this.antenna_types['antennas'][this.current_type]['vertex'];
|
|
ww.forEach(wire => {
|
|
//console.log(wire);
|
|
var vertices = new Float32Array(wire.length * 3);
|
|
var vidx = 0;
|
|
const scale_factor = 200.0; // Roughly pixels per wavelength
|
|
// Copy the vertex locations across into a Float32Array for the geometry:
|
|
wire.forEach((vertex) => {
|
|
//console.log(vertex, vv[vertex]);
|
|
vertices[vidx++] = (vv[vertex][0] * scale_factor);
|
|
vertices[vidx++] = (vv[vertex][1] * scale_factor);
|
|
vertices[vidx++] = (vv[vertex][2] * scale_factor);
|
|
});
|
|
//
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
|
// create a new wire with
|
|
const wire_line = new THREE.Line(geometry, material);
|
|
antenna_view.add(wire_line);
|
|
});
|
|
// Add the antenna into the scene:
|
|
return antenna_view;
|
|
};
|
|
}
|
|
|
|
// Density map class for calculating the CVF heat/density map:
|
|
class RadiationPattern {
|
|
constructor() {
|
|
// Heatmap specific stuff:
|
|
this.radiationPatternGeometry = new THREE.IcosahedronGeometry( 98, 5 );
|
|
this.radiationPatternVertices = this.radiationPatternGeometry.getAttribute('position');
|
|
this.radiationPatternHistogram = this.radiationPatternGeometry.getAttribute('uv');
|
|
//var radiationPatternColor = new Float32Array( radiationPatternVertices.count * 3 );
|
|
|
|
// Initialise to ZERO the UV values:
|
|
for(var i = 0; i < this.radiationPatternHistogram.count; i++) {
|
|
this.radiationPatternHistogram.setX(i, 0.0);
|
|
this.radiationPatternHistogram.setY(i, 0.0);
|
|
}
|
|
|
|
this.hiColor = [0, 255, 0, 1.0]; // Let's default to RED in [R, G, B, A]
|
|
this.loColor = [0, 0, 255, 1.0]; // Let's default to RED in [R, G, B, A]
|
|
this.contrast = 1.0;
|
|
this.shape = 1.0;
|
|
this.radiationPatternUniforms = {
|
|
//cameraConstant: { value: getCameraConstant( camera ) },
|
|
hiColor: { value: new THREE.Vector4(this.hiColor[0]/255.0, this.hiColor[1]/255.0, this.hiColor[2]/255.0, this.hiColor[3]) },
|
|
loColor: { value: new THREE.Vector4(this.loColor[0]/255.0, this.loColor[1]/255.0, this.loColor[2]/255.0, this.loColor[3]) },
|
|
maxHistogramValue: { value: 0.0 },
|
|
contrast: { value: this.contrast },
|
|
shape: { value: this.shape }
|
|
};
|
|
|
|
// ShaderMaterial
|
|
this.wireframe = true;
|
|
this.material = new THREE.ShaderMaterial( {
|
|
uniforms: this.radiationPatternUniforms,
|
|
vertexShader: radiationPatternVertexShader,
|
|
//fragmentShader: cvfFragmentShader,
|
|
wireframe: this.wireframe
|
|
} );
|
|
this.material.extensions.drawBuffers = true;
|
|
this.radiationPatternMesh = new THREE.Mesh( this.radiationPatternGeometry, this.material );
|
|
this.visibility = true;
|
|
this.radiationPatternMesh.visible = this.visibility;
|
|
|
|
this.hasWork = false;
|
|
}
|
|
|
|
// Insert the geometries into the scenegraph's sceneObject:
|
|
insertScene(sceneObject) {
|
|
sceneObject.add(this.radiationPatternMesh);
|
|
}
|
|
|
|
// Set the visible geometry. No point ever displaying both, as they use the SAME vertices:
|
|
setVisibility(value) {
|
|
this.visibility = value;
|
|
this.radiationPatternMesh.visible = value;
|
|
}
|
|
|
|
// Select whether to animate or not:
|
|
setAnimate(value) {
|
|
this.radiationPatternUniforms.animate.value = (value) ? true : false;
|
|
}
|
|
|
|
// Little hack for dat.gui. Update the color by passing 4-element array in form [255, 128, 0, 1.0]:
|
|
setHiColor(value) {
|
|
this.hiColor = value;
|
|
this.radiationPatternUniforms.hiColor.value = [this.hiColor[0]/255.0, this.hiColor[1]/255.0, this.hiColor[2]/255.0, this.hiColor[3]];
|
|
}
|
|
|
|
setLoColor(value) {
|
|
this.loColor = value;
|
|
this.radiationPatternUniforms.loColor.value = [this.loColor[0]/255.0, this.loColor[1]/255.0, this.loColor[2]/255.0, this.loColor[3]];
|
|
}
|
|
|
|
setPattern(ant_obj) {
|
|
// Populate the lookup table with the vertex and its index:
|
|
for(var i = 0; i < this.radiationPatternVertices.count; i++) {
|
|
const x = this.radiationPatternVertices.getX(i);
|
|
const y = this.radiationPatternVertices.getY(i);
|
|
const z = this.radiationPatternVertices.getZ(i);
|
|
// p at first should be a scalar. But later might be a composite of 2 scalars, to denote polarization (H, V)
|
|
var p = ant_obj.getPatternAt(x, y, z);
|
|
this.radiationPatternHistogram.setX(i, p);
|
|
this.radiationPatternHistogram.setY(i, p);
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
// Clear the geometry UV:
|
|
for(var i = 0; i < this.radiationPatternHistogram.count; i++) {
|
|
this.radiationPatternHistogram.setX(i, 0.0);
|
|
this.radiationPatternHistogram.setY(i, 0.0);
|
|
}
|
|
this.radiationPatternUniforms.maxHistogramValue.value = 0.0;
|
|
this.radiationPatternHistogram.needsUpdate = true;
|
|
this.i_n = 0;
|
|
this.i_m = 0;
|
|
this.i_phi = 0;
|
|
this.hasWork = true;
|
|
}
|
|
|
|
pause() {
|
|
this.hasWork = !this.hasWork;
|
|
}
|
|
|
|
// Function that solves the density map, but is capable of solving it in chunks of work, so as not to stall
|
|
// the screen refresh cycle. That way, high values of N x M x Phi, which can take minutes to solve, but
|
|
// still allows a dynamic and responsive user interface.
|
|
process(value) {
|
|
if(this.hasWork) {
|
|
var count = 0;
|
|
OUT:
|
|
for(; this.i_phi < this.PHI; ++this.i_phi) {
|
|
for(; this.i_m < this.M; ++this.i_m) {
|
|
for(; this.i_n < this.N; ++this.i_n) {
|
|
var geo = CVF.getY00Vertex (1, 98.0, this.i_n, this.N, this.i_m, this.M, this.i_phi, this.PHI);
|
|
|
|
// Obtain xyz coords of the CVF vertex:
|
|
var hx = Math.trunc(geo[0]/25.0) + 6;
|
|
var hy = Math.trunc(geo[1]/25.0) + 6;
|
|
var hz = Math.trunc(geo[2]/25.0) + 6;
|
|
|
|
// Look in the buckets/bins on either side of this point, to capture all vertices in range.
|
|
// This means looking in a 3x3 grid of bins to search. Still much quicker then iterating
|
|
// through ALL vertices:
|
|
for(var xx=-1; xx<2; ++xx) {
|
|
for(var yy=-1; yy<2; ++yy) {
|
|
for(var zz=-1; zz<2; ++zz) {
|
|
|
|
// Now find all the vertices in the bin, and increment its histogram:
|
|
for(var j=0, jl=this.fast_index[hx+xx][hy+yy][hz+zz].length; j < jl; ++j) {
|
|
var pos = this.fast_index[hx+xx][hy+yy][hz+zz][j];
|
|
var rangeSquared = Math.pow(geo[0] - this.radiationPatternVertices.getX(pos), 2)
|
|
+ Math.pow(geo[1] - this.radiationPatternVertices.getY(pos), 2)
|
|
+ Math.pow(geo[2] - this.radiationPatternVertices.getZ(pos), 2);
|
|
|
|
// Use a range of 15 units, so 15*15=225:
|
|
if(rangeSquared < 225.0) {
|
|
var num = this.radiationPatternHistogram.getX(pos);
|
|
this.radiationPatternHistogram.setX(pos, num+1.0);
|
|
|
|
// Find highest value in array:
|
|
if((num+1.0) > this.radiationPatternUniforms.maxHistogramValue.value) {
|
|
this.radiationPatternUniforms.maxHistogramValue.value = num+1.0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is where we check if we've completed a certain number of searches. If we have
|
|
// done more than "value", then we finish for now, and continue during the next
|
|
// process() function call, and continue where we left-off:
|
|
if(++count >= value) {
|
|
this.radiationPatternHistogram.needsUpdate = true;
|
|
break OUT;
|
|
}
|
|
}
|
|
// Finished the N-loop, so re-zero the iterator here:
|
|
this.i_n = 0;
|
|
}
|
|
// Finished the M-loop, so re-zero the iterator here:
|
|
this.i_m = 0;
|
|
}
|
|
// This checks the condition that we are completely done iterating through NxMxPHI. The needsUpdate
|
|
// variable is how we tell THREE.js to update/reload the geometry in VRAM (GPU buffers)
|
|
if(this.i_phi >= this.PHI) {
|
|
this.hasWork = false;
|
|
this.radiationPatternHistogram.needsUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
init();
|
|
animate();
|
|
|
|
// Setup the Three.js boiler-plate code to setup the screen/window, add controls, and EM-solve and
|
|
// display the default antenna.
|
|
function init() {
|
|
|
|
container = document.createElement( 'div' );
|
|
document.body.appendChild( container );
|
|
|
|
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 5, 15000 );
|
|
camera.position.y = 120;
|
|
camera.position.z = 400;
|
|
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0.0, 0.0, 0.0);
|
|
|
|
renderer = new THREE.WebGLRenderer({antialias:true});
|
|
renderer.setPixelRatio( window.devicePixelRatio );
|
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
|
container.appendChild( renderer.domElement );
|
|
|
|
controls = new OrbitControls( camera, renderer.domElement );
|
|
|
|
//stats = new Stats();
|
|
//container.appendChild( stats.dom );
|
|
window.addEventListener( 'resize', onWindowResize, false );
|
|
|
|
// Add an axis:
|
|
var axis = new THREE.AxesHelper(200);
|
|
scene.add(axis);
|
|
|
|
// Add the radiation pattern:
|
|
var pattern = new RadiationPattern();
|
|
pattern.insertScene(scene);
|
|
|
|
/*
|
|
var parameters =
|
|
{
|
|
a: 200, // numeric
|
|
b: 200, // numeric slider
|
|
c: "Hello, GUI!", // string
|
|
d: false, // boolean (checkbox)
|
|
e: "#ff8800", // color (hex)
|
|
f: function() { alert("Hello!") },
|
|
g: function() { alert( parameters.c ) },
|
|
v : 0, // dummy value, only type is important
|
|
w: "...", // dummy value, only type is important
|
|
x: 0, y: 0, z: 0
|
|
};
|
|
*/
|
|
|
|
var parameters = {
|
|
mode: true,
|
|
axis: true,
|
|
ground: true,
|
|
height: 50.0,
|
|
w: "...", // dummy value, only type is important
|
|
pattern: true,
|
|
};
|
|
|
|
const gui = new dat.GUI();
|
|
|
|
// Add a mode selector. View is to change perspective. Edit allows moving vertices. Solve runs the EM solver. (Maybe should be edit+solve?)
|
|
/*
|
|
gui.add(parameters, 'mode', ['view', 'edit', 'solve'])
|
|
.setValue('view')
|
|
.onChange(function(value){ console.log(value); });
|
|
*/
|
|
|
|
// Ground-plane
|
|
const geometry = new THREE.CircleGeometry( 150, 32 );
|
|
const material = new THREE.MeshBasicMaterial( { color: 0x006f00, wireframe: true } );
|
|
const ground_plane = new THREE.Mesh( geometry, material );
|
|
ground_plane.rotateX(Math.PI * -0.5);
|
|
scene.add( ground_plane );
|
|
|
|
// Add an enable axes-helper button:
|
|
gui.add(parameters, 'axis')
|
|
.setValue(true)
|
|
.onChange(function(value){ axis.visible = value; });
|
|
|
|
// Add an enable axes-helper button:
|
|
gui.add(parameters, 'ground')
|
|
.setValue(true)
|
|
.onChange(function(value){ ground_plane.visible = value; });
|
|
|
|
// Add an enable axes-helper button:
|
|
gui.add(parameters, 'height', 0.0, 500.0)
|
|
.setValue(parameters['height'])
|
|
.onChange(function(value){
|
|
//console.log(current_antenna_object['position']);
|
|
current_antenna_object['position'].y = value;
|
|
//console.log(parameters['height']);
|
|
});
|
|
|
|
gui.add( parameters, 'pattern')
|
|
.setValue(true)
|
|
.onChange(function(value){
|
|
pattern.setVisibility(value);
|
|
});
|
|
|
|
// Create the AntennaTypes object, which holds all the antenna types and creates the visual model on-request
|
|
ant = new AntennaTypes();
|
|
|
|
gui.add( parameters, 'w', ant.antenna_types['order'])
|
|
.setValue(ant.current_type)
|
|
.name('Types')
|
|
.onChange(function(value) {
|
|
console.log(value);
|
|
// Remove the current antenna visual model, if one was loaded:
|
|
scene.remove(current_antenna_object);
|
|
// Use selected antenna to rebuild new wire model:
|
|
ant.setAntennaType(value);
|
|
|
|
console.log(ant.getWires());
|
|
|
|
// Solve the z-matrix:
|
|
ant.calculateZMatrix();
|
|
console.log('Z', ant.Z);
|
|
|
|
var admittance = math.inv(ant.Z);
|
|
console.log('S', admittance);
|
|
|
|
var V = ant.createVoltageVector();
|
|
console.log('V', V);
|
|
|
|
var I = math.multiply(admittance, V);
|
|
console.log(I);
|
|
|
|
// Load new antenna visual model into the scene:
|
|
//console.log(ant.getWires());
|
|
current_antenna_object = ant.getThreeObject3D();
|
|
current_antenna_object['position'].y = parameters['height'];
|
|
scene.add(current_antenna_object);
|
|
});
|
|
|
|
// Add a folder for the antenna-specific controls.
|
|
//const cubeFolder = gui.addFolder('Antenna Controls');
|
|
|
|
/*
|
|
ant.antenna_types['order'].forEach(element => {
|
|
cubeFolder.add(element);
|
|
});
|
|
*/
|
|
|
|
// Create a half-wavelength long wire, with a radius of 0.001 lambda, and segmented into 10 pieces:
|
|
//wire = createWire(0.5, 0.0001, 45);
|
|
var wire = ant.getWires()[0];
|
|
//console.log(createWire(0.5, 0.0001, 45));
|
|
console.log(wire);
|
|
//console.log(wire);
|
|
//var frequency = 3.0e8;
|
|
//frequency = 1.0;
|
|
|
|
// Solve the z-matrix:
|
|
ant.calculateZMatrix();
|
|
console.log('Z', ant.Z);
|
|
var admittance = math.inv(ant.Z);
|
|
console.log('S', admittance);
|
|
var V = ant.createVoltageVector();
|
|
console.log('V', V);
|
|
var I = math.multiply(admittance, V);
|
|
console.log('I', I);
|
|
//V = math.multiply(impedance, I);
|
|
|
|
current_antenna_object = ant.getThreeObject3D();
|
|
current_antenna_object['position'].y = parameters['height'];
|
|
scene.add(current_antenna_object);
|
|
}
|
|
|
|
//
|
|
function createWire(length, wire_radius, segments) {
|
|
// dimensions in lambda
|
|
var wire = {};
|
|
wire.length = length;
|
|
wire.seg_len = length / segments;
|
|
wire.radius = wire_radius;
|
|
const offset = 0.5 * length;
|
|
wire.points = [];
|
|
wire.points.push([0.0, 0.0, -offset]);
|
|
for (let i = 0; i < segments; i++) {
|
|
wire.points.push([0.0, 0.0, i * wire.seg_len + 0.5 * wire.seg_len - offset]);
|
|
wire.points.push([0.0, 0.0, (i+1) * wire.seg_len - offset]);
|
|
}
|
|
return wire;
|
|
}
|
|
|
|
function psi_old(wire, n, m) {
|
|
var retval = 0.0;
|
|
const k = 2.0 * Math.PI; // Normalised wavelength is equal to 1.0 - otherwise 2*pi/wavelength
|
|
const fourPI = 4.0 * Math.PI;
|
|
var Rmn = 0.0;
|
|
// From MININEC thesis (3-36) and (3-37):
|
|
if(m==n) {
|
|
Rmn = Math.sqrt(wire.radius**2 + (wire.seg_len*0.5)**2);
|
|
} else {
|
|
Rmn = Math.sqrt((wire.points[m][0] - wire.points[n][0])**2 + (wire.points[m][1] - wire.points[n][1])**2 + (wire.points[m][2] - wire.points[n][2])**2);
|
|
}
|
|
retval = math.multiply(math.complex(Math.cos(k * Rmn), -Math.sin(k * Rmn)), (1/(fourPI*Rmn)));
|
|
//console.log(n, m, retval);
|
|
return retval;
|
|
}
|
|
|
|
// Use Harrington's equations (129) and (135):
|
|
function psi2(wire, n, m) {
|
|
var retval = 0.0;
|
|
var Rmn = 0.0;
|
|
// Calculate the range from the source point (n) to the observation point (m) depending whether it is the same segment or not:
|
|
if(m==n) {
|
|
Rmn = Math.sqrt(wire.radius**2 + (wire.seg_len*0.5)**2);
|
|
} else {
|
|
Rmn = Math.sqrt((wire.points[m][0] - wire.points[n][0])**2 + (wire.points[m][1] - wire.points[n][1])**2 + (wire.points[m][2] - wire.points[n][2])**2);
|
|
}
|
|
// Now if r<10a, use 129. If r>=10a, use 135:
|
|
const alpha = wire.seg_len*0.5;
|
|
const zeta = wire.points[m][2] - wire.points[n][2]; // This is z at m when n is set as the coordinate space origin. So need to transform coord-space to make it N-centric first! Uugh!
|
|
// zeta is the projection of m onto the n segment, if the n-segment were centered at the origin along the z-direction.
|
|
const mn = math.subtract(wire.points[m], wire.points[n-1])
|
|
//var zeta = math.dot(wire.points[m], wire.points[n]);
|
|
//zeta = zeta / ()
|
|
const rho = 0;
|
|
if(Rmn < (10.0 * alpha)) {
|
|
// Eq 129:
|
|
var t1 = math.complex(Math.cos(k * Rmn), -Math.sin(k * Rmn));
|
|
t1 = math.multiply((1.0/(8.0*Math.PI*alpha)), t1);
|
|
const i1 = Math.log((zeta + alpha + Math.sqrt(rho**2 + (zeta + alpha)**2)) / (zeta - alpha + Math.sqrt(rho**2 + (zeta - alpha)**2)));
|
|
const i2 = 2 * alpha;
|
|
const i3 = (0.5 * (alpha + zeta)) * Math.sqrt(rho**2 + (alpha + zeta)**2) + (0.5 * (alpha - zeta)) * Math.sqrt(rho**2 + (zeta - alpha)**2) + (0.5 * rho**2 * i1);
|
|
const i4 = (2*alpha*rho**2) + (0.333333 * (2*alpha**3 + 6*alpha*zeta**2));
|
|
const re = i1 - 0.5*k**2 * (i3 - 2*Rmn*i2 + Rmn**2*i1);
|
|
const im = -k*(i2 - Rmn*i1) + (1.0/6)*k**3*(i4 - 3*Rmn*i3 + 3*Rmn**2*i2 - Rmn**3*i1);
|
|
retval = math.complex(math.multiply(t1, re), math.multiply(t1, im));
|
|
} else {
|
|
// Eq 135:
|
|
const a_r = alpha / rho;
|
|
const z_r = zeta / rho;
|
|
const A0 = 1 + (1.0/6.0) * (a_r ** 2) * (-1 + 3 * z_r**2) + (1/40) * a_r ** 4 * (3 - 30 * z_r**2 + 35 * z_r**4);
|
|
const A1 = (1/6)*a_r*(-1 + 3 * z_r**2) + (1/40)*a_r**3 * (3 - 30 * z_r**2 + 35 * z_r**4);
|
|
const A2 = (-1/6)*(z_r)**2 - (1/40) * (a_r)**2 * (1 - 12*z_r**2 + 15 * z_r**4);
|
|
const A3 = (1/60)*a_r*(3*z_r**2 - 5 * z_r**4);
|
|
const A4 = (1/120)*(z_r**4);
|
|
const ka = k * alpha;
|
|
const kr = k * rho;
|
|
const re = (B*Math.cos(kr)) * (A0 + ka**2 * A2 + ka**4 * A4);
|
|
const im = (B*Math.sin(kr)) * (ka*A1 + ka**3 * A3);
|
|
retval = math.complex(re + im);
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
function onWindowResize() {
|
|
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize( window.innerWidth, window.innerHeight );
|
|
|
|
// TODO - need to notify scene object shaders (via uniforms) here if required!
|
|
// becvfUniforms.cameraConstant.value = getCameraConstant( camera );
|
|
}
|
|
|
|
function getCameraConstant( camera ) {
|
|
return window.innerHeight / ( Math.tan( THREE.Math.DEG2RAD * 0.5 * camera.fov ) / camera.zoom );
|
|
}
|
|
|
|
function animate() {
|
|
// Update the elapsed time to later provide to the shaders:
|
|
var delta = clock.getDelta();
|
|
tick += delta;
|
|
if ( tick < 0 ) tick = 0;
|
|
|
|
// Tell WebGL to call the 'animate()' function for the next screen refresh:
|
|
requestAnimationFrame( animate );
|
|
|
|
// Render the scene, and update the stats:
|
|
renderer.render( scene, camera );
|
|
//stats.update();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|