By Markus Sprunck; Revision: 1.2; Status: final; Last Content Change: Apr 9, 2013;
This sample application demonstrates the simplicity of Finite Element Method (FEM) for 2D structural mechanics. All the code is written in pure Java without additional libraries. Just the code for visualization is written in JavaScript and it draws directly to Canvas of HTML5 with support of Paper.js library. Expected ResultThe following screen shot shows the result of a simple beam. A cantilever beam can be solved analytically and serves as a test case for the FEM simulation. Figure 1: Display of the simple default FEM model
Finite Element Method BasicsIf you are familiar with the basics you may skip this chapter. In principle FEM is quite simple. All is about creating a large linear equation system that is an approximation of the real physical behavior. The following four steps show a simplified view of what happens in structural mechanics - starting with a simple spring and ending with a solid triangular element for FEM. Figure 2: Simple spring follows one linear equation Wikipedia says "In 1676 British physicist Robert Hooke discovered the principle behind springs' action, that the force it exerts is proportional to its extension, now called Hooke's law." [1] So this is not really new. Figure 4: Three springs have six linear equations and the stiffness matrix has just a filled diagonal. Putting now three springs together to a triangle leads to six linear equations, because you have now tree points with two degrees of freedom. The stiffness matrix of this system is very simple, each node sums up the stiffness components from two springs. Figure 4: A triangle has six linear equations and the stiffness matrix is more complex. A solid triangular has the same equations just the element stiffness matrix looks different. It is dependent on the material and geometry. Now there a just two additional things to do.
The code was developed with Eclipse Juno (see also Download) and needs no additional Java libraries. All really interesting happens in the following class Solver.
// File #4: Matrix.java - helper class for matrix manipulations
// File #5: ModelUtil.java - creates the default model and supports IO package com.sprunck.fem.io; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import com.sprunck.fem.core.Model; public class ModelUtil { public static int parseModelFromString(Model femCore, String input) { class Point { public double x = 0.0f; public double y = 0.0f; } final List<Point> tempNodes = new LinkedList<Point>(); tempNodes.add(new Point()); int bandwidthExpected = 0; final List<Integer[]> tempElements = new LinkedList<Integer[]>(); tempElements.add(new Integer[5]); final String[] lines = input.toString().split("\\n"); for (final String line : lines) { if (!line.trim().isEmpty()) { final String[] args = line.split(","); if (0 == args[0].trim().compareToIgnoreCase("N")) { femCore.incementNumberOfNodes(); final int number = Integer.valueOf(args[1].trim()); for (int index = tempNodes.size(); index <= number; index++) { tempNodes.add(new Point()); } final Integer first = Integer.valueOf(args[2].trim()); final Integer second = Integer.valueOf(args[3].trim()); tempNodes.get(number).x = first; tempNodes.get(number).y = second; } if (0 == args[0].trim().compareToIgnoreCase("E")) { femCore.incementNumberOfElements(); final int number = Integer.valueOf(args[1].trim()); for (int index = tempElements.size(); index <= number; index++) { tempElements.add(new Integer[5]); } final Integer first = Integer.valueOf(args[2].trim()); final Integer second = Integer.valueOf(args[3].trim()); final Integer third = Integer.valueOf(args[4].trim()); tempElements.get(femCore.getNumberOfElements())[1] = number; tempElements.get(femCore.getNumberOfElements())[2] = first; tempElements.get(femCore.getNumberOfElements())[3] = second; tempElements.get(femCore.getNumberOfElements())[4] = third; final int max = Math.max(Math.max(first, second), third); final int min = Math.min(Math.min(first, second), third); final int bandwidthOfElement = (1 + max - min) * 2; bandwidthExpected = Math.max(bandwidthExpected, bandwidthOfElement); } if (0 == args[0].trim().compareToIgnoreCase("D")) { if (femCore.getDeltasIn() == null) { femCore.initDeltasIn(); } final int number = Integer.valueOf(args[1].trim()); if (0 == args[2].trim().compareToIgnoreCase("x")) { femCore.setDeltaInX(number, Double.valueOf(args[3].trim())); } if (0 == args[2].trim().compareToIgnoreCase("y")) { femCore.setSeltaInY(number, Double.valueOf(args[3].trim())); } } if (0 == args[0].trim().compareToIgnoreCase("F")) { if (femCore.getForcesIn() == null) { femCore.initForcesIn(); } final int number = Integer.valueOf(args[1].trim()); if (0 == args[2].trim().compareToIgnoreCase("x")) { femCore.setForceInX(number, Double.valueOf(args[3].trim())); } if (0 == args[2].trim().compareToIgnoreCase("y")) { femCore.setForceInY(number, Double.valueOf(args[3].trim())); } } } } femCore.initNodesByElement(); for (int i = 1; i <= femCore.getNumberOfElements(); i++) { femCore.setX(tempElements.get(i)[1], 1, tempNodes.get(tempElements.get(i)[2]).x); femCore.setX(tempElements.get(i)[1], 2, tempNodes.get(tempElements.get(i)[3]).x); femCore.setX(tempElements.get(i)[1], 3, tempNodes.get(tempElements.get(i)[4]).x); femCore.setY(tempElements.get(i)[1], 1, tempNodes.get(tempElements.get(i)[2]).y); femCore.setY(tempElements.get(i)[1], 2, tempNodes.get(tempElements.get(i)[3]).y); femCore.setY(tempElements.get(i)[1], 3, tempNodes.get(tempElements.get(i)[4]).y); femCore.setNodeId(tempElements.get(i)[1], 1, tempElements.get(i)[2]); femCore.setNodeId(tempElements.get(i)[1], 2, tempElements.get(i)[3]); femCore.setNodeId(tempElements.get(i)[1], 3, tempElements.get(i)[4]); } return bandwidthExpected; } public static String getModelAsJSON(Model model) { final HashMap<Integer, Boolean> nodeIds = new HashMap<Integer, Boolean>(); final StringBuilder pre = new StringBuilder("var elements = ["); final int numberOfElements = model.getNumberOfElements(); for (int elementId = 1; elementId <= numberOfElements; elementId++) { pre.append("["); for (int cornerId = 1; cornerId < 4; cornerId++) { final int nodeId = model.getNodeId(elementId, cornerId); pre.append("\n{\"id\": " + nodeId // + ", \"x_force\" : " + model.getForceOutX(nodeId) // + ", \"y_force\" : " + model.getForceOutY(nodeId) // + ", \"x_delta\" : " + model.getDeltaOutX(nodeId) // + ", \"y_delta\" : " + model.getDeltaOutY(nodeId) // + ", \"x_fixed\" : " + model.isFixedX(nodeId) // + ", \"y_fixed\" : " + model.isFixedY(nodeId) // + ", \"first\" : " + !nodeIds.containsKey(nodeId) // + ", \"x\" : " + model.getX(elementId, cornerId) // + ", \"y\" : " + model.getY(elementId, cornerId) // + ", \"y_delta_mean\" : " + model.getDeltaYMean(elementId) // + " }"); if (cornerId <= 3) { pre.append(','); } nodeIds.put(nodeId, true); } pre.append("]"); if (elementId < numberOfElements + 1) { pre.append(','); } } pre.append("];"); return pre.toString(); } public static String createDefaultModel(Model femCore) { final StringBuffer nodeText = new StringBuffer(); final int maxCols = 20; final int maxRows = 5; final int scaleFactorX = 10; final int scaleFactorY = 10; for (int col = 1; col <= maxCols; col++) { for (int row = 1; row <= maxRows; row++) { final int nodeId = row + maxRows * (col - 1); nodeText.append("N, ").append(nodeId).append(", ") .append(col * scaleFactorX).append(", ") .append(row * scaleFactorY).append(",\n"); } } for (int col = 1; col < maxCols; col++) { for (int row = 1; row < maxRows; row++) { final int firstElementId = row * 2 - 1 + (maxRows - 1) * 2 * (col - 1); final int secondElementId = row * 2 + (maxRows - 1) * 2 * (col - 1); final int node1Id = row + maxRows * (col - 1); final int node2Id = row + maxRows * col; final int node3Id = row + 1 + maxRows * (col - 1); final int node4Id = row + 1 + maxRows * (col + 1 - 1); nodeText.append("E, ").append(firstElementId).append(", ") .append(node1Id).append(", ").append(node2Id) .append(", ").append(node3Id).append(",\n"); nodeText.append("E, ").append(secondElementId).append(", ") .append(node2Id).append(", ") .append(node4Id).append(", ").append(node3Id).append(",\n"); } } nodeText.append("D, ").append(1).append(", y, ").append(0).append(",\n"); for (int row = 1; row <= maxRows; row++) { nodeText.append("D, ").append(row).append(", x, ").append(0).append(",\n"); } nodeText.append("F, ").append(maxRows * maxCols).append(", y, ") .append(30000.0).append(",\n"); return nodeText.toString(); } } // File #6: ResultTemplate.html - for visualization <!DOCTYPE html> <html> <head> <script type="text/javascript" src="rendermodel.js"></script> <script type="text/javascript"> // Next line will be replaced by the JSON model, don't change it ... XX_MODEL_PLACE_HOLDER; // Only executed our code once the DOM is ready. window.onload = function() { var canvas = document.getElementById('myCanvas'); paper.setup(canvas); main(elements); paper.view.draw(); } </script> </head> <body> <canvas id="myCanvas" resize></canvas> </body> </html> includeJavaScript('paper.js');
// Dimensions for display
var height = 400;
var width = 750;
// Scaling factors for stretch or compress during painting
var factorX = 2.5;
var factorY = 2.5;
var factorForce = 0.0005;
var factordelta = 10;
// Offset for model
var offset_x = 40;
var offset_y = 40;
// Offset and size for scale boxs
var offset_x_scale = width - 150;
var scale_size_x = 25;
var scale_size_y = 250;
// Fixed scale
var minColor = 10;
var maxColor = 0;
function main(elements) {
// draw all elements
for ( var ele = 0; ele < elements.length; ele++) {
var path = new paper.Path();
path.closed = true;
path.strokeColor = '#FFFFFF';
path.strokeWidth = 0.2;
path.selected = false;
path.opacity = 0.9;
for ( var nodeId = 0; nodeId < elements[ele].length; nodeId++) {
var element = elements[ele][nodeId];
path.fillColor = getColor(element.y_delta_mean);
var point = new paper.Point(element.x * factorX + offset_x
+ element.x_delta * factordelta, element.y * factorY
+ element.y_delta * factordelta + offset_y);
path.add(point);
}
ele.first = true;
}
for ( var ele = 0; ele < elements.length; ele++) {
var path = new paper.Path();
for ( var nodeId = 0; nodeId < elements[ele].length; nodeId++) {
var element = elements[ele][nodeId];
var point = new paper.Point(element.x * factorX + offset_x
+ element.x_delta * factordelta, element.y * factorY
+ element.y_delta * factordelta + offset_y);
// ensure that all is just drawn once
if (element.first) {
var text = new paper.PointText(point);
text.content = ' ' + element.id;
text.fontSize = 6;
text.justification = 'left';
if (10000.0 < Math.abs(element.x_force)) {
drawVector(point, point.add(new paper.Point(element.x_force
* factorForce, 0)), true, (element.x_force > 0.0));
}
if (10000.0 < Math.abs(element.y_force)) {
drawVector(point, point.add(new paper.Point(0,
element.y_force * factorForce)), false,
(element.y_force > 0.0));
}
drawFixed(point, element.y_fixed, element.x_fixed);
}
}
}
drawBoxes();
drawScale();
}
function drawVector(vectorStart, vectorEnd, horizontal, positive) {
var arrow = new paper.Path();
arrow.strokeWidth = 1.5;
arrow.strokeColor = '#FF0000';
arrow.add(vectorStart);
arrow.add(vectorEnd);
arrow.opacity = 1;
var length = 5;
var head = new paper.Path();
head.strokeWidth = 1.0;
head.strokeColor = '#FF0000';
head.add(vectorEnd);
head.opacity = 1;
if (horizontal && !positive) {
head.add(vectorEnd.add(new paper.Point(length, -length)));
head.add(vectorEnd);
head.add(vectorEnd.add(new paper.Point(length, length)));
} else if (horizontal && positive) {
head.add(vectorEnd.add(new paper.Point(-length, -length)));
head.add(vectorEnd);
head.add(vectorEnd.add(new paper.Point(-length, length)));
} else if (!horizontal && positive) {
head.add(vectorEnd.add(new paper.Point(length, -length)));
head.add(vectorEnd);
head.add(vectorEnd.add(new paper.Point(-length, -length)));
} else if (!horizontal && !positive) {
head.add(vectorEnd.add(new paper.Point(-length, length)));
head.add(vectorEnd);
head.add(vectorEnd.add(new paper.Point(length, length)));
}
}
function drawFixed(vectorEnd, horizontal, vertical) {
var length = 7;
if (horizontal) {
var head = new paper.Path();
head.strokeWidth = 0.5;
head.strokeColor = '#000000';
head.add(vectorEnd);
head.add(vectorEnd.add(new paper.Point(length * 0.75, length)));
head.add(vectorEnd.add(new paper.Point(-length * 0.75, length)));
head.add(vectorEnd);
}
if (vertical) {
var head = new paper.Path();
head.strokeWidth = 0.5;
head.strokeColor = '#000000';
head.add(vectorEnd);
head.add(vectorEnd.add(new paper.Point(-length, -length * 0.75)));
head.add(vectorEnd.add(new paper.Point(-length, length * 0.75)));
head.add(vectorEnd);
}
}
function drawBoxes() {
var path = new paper.Path();
path.closed = true;
path.strokeWidth = 0.25;
path.strokeColor = '#000000';
path.selected = false;
path.add(new paper.Point(0, 0));
path.add(new paper.Point(0, height));
path.add(new paper.Point(offset_x_scale - 5, height));
path.add(new paper.Point(offset_x_scale - 5, 0));
var path2 = new paper.Path();
path2.closed = true;
path2.strokeWidth = 0.25;
path2.strokeColor = '#000000';
path2.selected = false;
path2.add(new paper.Point(width, 0));
path2.add(new paper.Point(width, height));
path2.add(new paper.Point(offset_x_scale, height));
path2.add(new paper.Point(offset_x_scale, 0));
}
function drawScale() {
var path = new paper.Path();
var text = new paper.PointText(new paper.Point(10+offset_x_scale, 20));
text.content = 'Average delta in y: ';
text.justification = 'left';
var delta = 0.10;
for ( var index = 0.0; index <= 1.0; index += delta) {
var path = new paper.Path();
path.add(new paper.Point(10+offset_x_scale,
40 + index * scale_size_y));
path.add(new paper.Point(10+offset_x_scale,
40 + (index + delta) * scale_size_y));
path.add(new paper.Point(10+offset_x_scale + scale_size_x,
40 + (index + delta) * scale_size_y));
path.add(new paper.Point(10+offset_x_scale + scale_size_x,
40 + index* scale_size_y));
var text = new paper.PointText(
new paper.Point(10+offset_x_scale+ scale_size_x * 1.2,
40 + (index + delta / 2) * scale_size_y));
var value = (minColor + (maxColor - minColor) * index);
text.content = ' ' + (Math.round(100 * value) / 100.0) + ' mm';
text.fontSize = 8;
text.justification = 'left';
path.closed = true;
path.strokeWidth = 0.5;
path.fillColor = getColor(value);
path.opacity = 0.8;
path.selected = false;
}
}
// Color code helper /////////////////////////////////////////////////////////
var frequency = 0.5;
function getColor(value) {
red = Math.sin(2 - frequency * value) * 127 + 128;
green = Math.sin(1 - frequency * value) * 127 + 128;
blue = Math.sin(4 - frequency * value) * 127 + 128;
return '#' + integerToHex(red) + integerToHex(green) + integerToHex(blue);
}
function integerToHex(n) {
n = Math.max(0, Math.min(parseInt(n, 10), 255));
charFirst = "0123456789ABCDEF".charAt((n - n % 16) / 16);
charSecond = "0123456789ABCDEF".charAt(n % 16);
return charFirst + charSecond;
}
// Include helper ////////////////////////////////////////////////////////////
function includeJavaScript(filename) {
// helper for lazy load of additional JavaScript libraries
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.src = filename;
script.type = 'text/javascript';
head.appendChild(script)
}
// File #8: WebServer.java - just a helper for test (with main method of the application) package com.sprunck.fem.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import com.sprunck.fem.core.Model;
import com.sprunck.fem.core.Solver;
public class WebServer extends Thread {
private static final String NL = System.getProperty("line.separator");
private final int port = 1234;
private final Model sf;
public WebServer(Model sf) {
this.sf = sf;
String hostname = "localhost";
try {
try {
final InetAddress addr = InetAddress.getLocalHost();
hostname = addr.getCanonicalHostName();
} catch (final UnknownHostException e) {
System.out.println("Error - " + e.getMessage() + NL);
}
final InetAddress addr = InetAddress.getLocalHost();
hostname = addr.getCanonicalHostName();
addr.getCanonicalHostName();
} catch (final UnknownHostException e) {
System.out.println(e.getMessage());
}
// System.out.println("listen at url: http://localhost:1234/index.html");
System.out.println("webserver started [http://" + hostname + ':'
+ port + "/index.html]");
final Thread serverThread = this;
serverThread.start();
}
@Override
public void run() {
Socket connection = null;
while (true) {
try {
final ServerSocket server = new ServerSocket(port);
connection = server.accept();
final OutputStream out =
new BufferedOutputStream(connection.getOutputStream());
final InputStream in =
new BufferedInputStream(connection.getInputStream());
final String request = readFirstLineOfRequest(in).toString();
// System.out.println("get request " + request.toString());
if (request.toLowerCase().startsWith("get /index.html")) {
// Create content of response
final String contentText =
getPage("com/sprunck/fem/io/ResultTemplate.html").toString();
final byte[] content = contentText.getBytes();
// For HTTP/1.0 or later send a MIME header
if (request.indexOf("HTTP/") != -1) {
final String headerString = "HTTP/1.0 200 OK" + NL
+ "Server: FEM 1.0" + NL
+ "Content-length: " + content.length + NL
+ "Content-type: text/html" + NL + NL;
final byte[] header = headerString.getBytes("ASCII");
out.write(header);
}
out.write(content);
out.flush();
} else if (request.toLowerCase().startsWith("get /paper.js")) {
// Create content of response
final String contentText =
getPage("com/sprunck/fem/io/paper.js").toString();
final byte[] content = contentText.getBytes();
// For HTTP/1.0 or later send a MIME header
if (request.indexOf("HTTP/") != -1) {
final String headerString = "HTTP/1.0 200 OK" + NL
+ "Server: FEM 1.0" + NL
+ "Content-length: " + content.length + NL
+ "Content-type: text/javascript" + NL + NL;
final byte[] header = headerString.getBytes("ASCII");
out.write(header);
}
out.write(content);
out.flush();
} else if (request.toLowerCase().startsWith("get /rendermodel.js")) {
// Create content of response
final String contentText =
getPage("com/sprunck/fem/io/renderModel.js").toString();
final byte[] content = contentText.getBytes();
// For HTTP/1.0 or later send a MIME header
if (request.indexOf("HTTP/") != -1) {
final String headerString = "HTTP/1.0 200 OK" + NL
+ "Server: FEM 1.0" + NL
+ "Content-length: " + content.length + NL
+ "Content-type: text/javascript" + NL + NL;
final byte[] header = headerString.getBytes("ASCII");
out.write(header);
}
out.write(content);
out.flush();
} else if (request.startsWith("GET /terminate")) {
server.close();
throw new RuntimeException("terminate");
}
// Close the socket
connection.close();
in.close();
out.close();
server.close();
} catch (final IOException e) {
System.out.println(e.getMessage());
}
}
}
private StringBuffer readFirstLineOfRequest(final InputStream in)
throws IOException {
final StringBuffer request;
request = new StringBuffer(100);
while (true) {
final int character = in.read();
if (character == '\n' || character == '\r' || character == -1) {
break;
}
request.append((char) character);
}
return request;
}
public StringBuffer getPage(String filepath) {
final StringBuffer fw = new StringBuffer(1000);
try {
final InputStream inputstream =
this.getClass().getClassLoader().getResourceAsStream(filepath);
final InputStreamReader is = new InputStreamReader(inputstream);
final BufferedReader br = new BufferedReader(is);
for (String s = br.readLine(); s != null; s = br.readLine()) {
if (s.contains("XX_MODEL_PLACE_HOLDER")) {
fw.append(ModelUtil.getModelAsJSON(sf));
} else {
fw.append(s).append('\n');
}
}
inputstream.close();
} catch (final Exception xc) {
xc.printStackTrace();
}
return fw;
}
public static void main(String[] args) {
final Solver fem = new Solver();
fem.run(ModelUtil.createDefaultModel(fem));
new WebServer(fem);
}
}
Please, do not hesitate to contact me if you have any ideas for improvement and/or you find a bug in the sample code.
Refererences[1] Wikipedia - Spring (device);http://en.wikipedia.org/wiki/Spring_(device) http://en.wikipedia.org/wiki/Matrix_(mathematics)
|




