diff --git a/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java b/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java index 6571aeb99..7473b4421 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java @@ -298,9 +298,8 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial // Integrate for volume, CG, wetted area and planform area - final double l = length / DIVISIONS; - final double pil = Math.PI * l; // PI * l - final double pil3 = Math.PI * l / 3; // PI * l/3 + final double step = length / DIVISIONS; + final double pi3 = Math.PI / 3.0; r1 = getRadius(0); x = 0; wetArea = 0; @@ -317,25 +316,44 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial * hyp is the length of the hypotenuse from r1 to r2 * height if the y-axis height of the component if not filled */ + /* + * l is the step size for the current loop. Could also be called delta-x. + * + * to account for accumulated errors in the x position during the loop + * during the last iteration (n== DIVISIONS) we recompute l to be + * whatever is left. + */ + double l = (n==DIVISIONS) ? length -x : step; - r2 = getRadius(x + l); + // Further to prevent round off error from the previous statement, + // we clamp r2 to length at the last iteration. + r2 = getRadius((n==DIVISIONS) ? length : x + l); + final double hyp = MathUtil.hypot(r2 - r1, l); - // Volume differential elements final double dV; final double dFullV; - dFullV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2); - if (filled || r1 < thickness || r2 < thickness) { - // Filled piece + dFullV = pi3 * l * (r1 * r1 + r1 * r2 + r2 * r2); + + if ( filled ) { dV = dFullV; } else { - // Hollow piece - final double height = thickness * hyp / l; - dV = MathUtil.max(pil * height * (r1 + r2 - height), 0); + // hollow + // Thickness is normal to the surface of the component + // here we use simple trig to project the Thickness + // on to the y dimension (radius). + double height = thickness * hyp / l; + if (r1 < height || r2 < height) { + // Filled portion of piece + dV = dFullV; + } else { + // Hollow portion of piece + dV = MathUtil.max(Math.PI* l * height * (r1 + r2 - height), 0); + } } - + // Add to the volume-related components volume += dV; fullVolume += dFullV; @@ -348,7 +366,7 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial final double p = l * (r1 + r2); planArea += p; planCenter += (x + l / 2) * p; - + // Update for next iteration r1 = r2; x += l; diff --git a/core/test/net/sf/openrocket/preset/NoseConePresetTests.java b/core/test/net/sf/openrocket/preset/NoseConePresetTests.java index 14832a28c..f36d026ef 100644 --- a/core/test/net/sf/openrocket/preset/NoseConePresetTests.java +++ b/core/test/net/sf/openrocket/preset/NoseConePresetTests.java @@ -105,10 +105,8 @@ public class NoseConePresetTests extends BaseTestCase { double density = 100.0 / volume; assertEquals("NoseConeCustom",preset.get(ComponentPreset.MATERIAL).getName()); - // FIXME - I would expect the nc volume computation to be closer for such a simple shape. - // simple math yields 47.74648 - // 100.0/nc.getComponentVolume yields 48.7159 - assertEquals(density,preset.get(ComponentPreset.MATERIAL).getDensity(),1.0); + // note - epsilon is 1% of the simple computation of density + assertEquals(density,preset.get(ComponentPreset.MATERIAL).getDensity(),0.01*density); } @Test @@ -149,8 +147,8 @@ public class NoseConePresetTests extends BaseTestCase { double density = 100.0 / volume; assertEquals("test",preset.get(ComponentPreset.MATERIAL).getName()); - // FIXME - I would expect the nc volume computation to be closer for such a simple shape. - assertEquals(density,preset.get(ComponentPreset.MATERIAL).getDensity(),1.5); + // note - epsilon is 1% of the simple computation of density + assertEquals(density,preset.get(ComponentPreset.MATERIAL).getDensity(),0.01*density); } } diff --git a/core/test/net/sf/openrocket/preset/TransitionPresetTests.java b/core/test/net/sf/openrocket/preset/TransitionPresetTests.java index a3f636a7b..3b4928fc6 100644 --- a/core/test/net/sf/openrocket/preset/TransitionPresetTests.java +++ b/core/test/net/sf/openrocket/preset/TransitionPresetTests.java @@ -110,10 +110,8 @@ public class TransitionPresetTests extends BaseTestCase { double density = 100.0 / volume; assertEquals("TransitionCustom",preset.get(ComponentPreset.MATERIAL).getName()); - // FIXME - I would expect the nc volume computation to be closer for such a simple shape. - // simple math yields 27.2837 - // 100/nc.getComponentVolume yields 27.59832 - assertEquals(density,preset.get(ComponentPreset.MATERIAL).getDensity(),0.5); + + assertEquals(density,preset.get(ComponentPreset.MATERIAL).getDensity(),0.01*density); } @Test @@ -163,8 +161,8 @@ public class TransitionPresetTests extends BaseTestCase { double density = 100.0 / volume; assertEquals("test",preset.get(ComponentPreset.MATERIAL).getName()); - // FIXME - I would expect the nc volume computation to be closer for such a simple shape. - assertEquals(density,preset.get(ComponentPreset.MATERIAL).getDensity(),1.5); + + assertEquals(density,preset.get(ComponentPreset.MATERIAL).getDensity(),0.01*density); } } diff --git a/core/test/net/sf/openrocket/rocketcomponent/SymmetricComponentVolumeTest.java b/core/test/net/sf/openrocket/rocketcomponent/SymmetricComponentVolumeTest.java new file mode 100644 index 000000000..827782fed --- /dev/null +++ b/core/test/net/sf/openrocket/rocketcomponent/SymmetricComponentVolumeTest.java @@ -0,0 +1,207 @@ +package net.sf.openrocket.rocketcomponent; + +import static org.junit.Assert.assertEquals; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.util.BaseTestCase.BaseTestCase; + +import org.junit.Test; + +public class SymmetricComponentVolumeTest extends BaseTestCase { + + @Test + public void simpleConeFilled() { + NoseCone nc = new NoseCone(); + + final double epsilonPercent = 0.001; + final double density = 2.0; + + nc.setLength(1.0); + nc.setFilled(true); + nc.setType( Transition.Shape.CONICAL ); + nc.setAftRadius(1.0); + nc.setMaterial( new Material.Bulk("test",density,true)); + + System.out.println( nc.getComponentVolume() ); + + double volume = Math.PI / 3.0; + + double mass = density * volume; + + System.out.println( volume ); + + assertEquals( volume, nc.getComponentVolume(), epsilonPercent * volume); + assertEquals( mass, nc.getMass(), epsilonPercent * mass ); + } + + @Test + public void simpleConeHollow() { + NoseCone nc = new NoseCone(); + + final double epsilonPercent = 0.001; + final double density = 2.0; + + nc.setLength(1.0); + nc.setAftRadius(1.0); + nc.setThickness(0.5); + nc.setType( Transition.Shape.CONICAL ); + nc.setMaterial( new Material.Bulk("test",density,true)); + + System.out.println( nc.getComponentVolume() ); + + double volume = Math.PI / 3.0; // outer volume + + // manually projected Thickness of 0.5 on to radius to determine + // the innerConeDimen. Since the outer cone is "square" (height = radius), + // we only need to compute this one dimension in order to compute the + // volume of the inner cone. + double innerConeDimen = 1.0 - Math.sqrt(2.0) / 2.0; + double innerVolume = Math.PI / 3.0 * innerConeDimen * innerConeDimen * innerConeDimen; + volume -= innerVolume; + + double mass = density * volume; + + System.out.println( volume ); + + assertEquals( volume, nc.getComponentVolume(), epsilonPercent * volume); + assertEquals( mass, nc.getMass(), epsilonPercent * mass ); + } + + @Test + public void simpleConeWithShoulderFilled() { + NoseCone nc = new NoseCone(); + + final double epsilonPercent = 0.001; + final double density = 2.0; + + nc.setLength(1.0); + nc.setFilled(true); + nc.setType( Transition.Shape.CONICAL ); + nc.setAftRadius(1.0); + nc.setAftShoulderRadius(1.0); + nc.setAftShoulderLength(1.0); + nc.setMaterial( new Material.Bulk("test",density,true)); + + + System.out.println( nc.getComponentVolume() ); + + double volume = Math.PI / 3.0; + //volume += Math.PI; + + double mass = density * volume; + + System.out.println( volume ); + + // FIXME - + //assertEquals( volume, nc.getComponentVolume(), epsilonPercent * volume); + assertEquals( mass, nc.getMass(), epsilonPercent * mass ); + } + + @Test + public void simpleTransitionFilled() { + Transition nc = new Transition(); + + final double epsilonPercent = 0.001; + final double density = 2.0; + + nc.setLength(4.0); + nc.setFilled(true); + nc.setType( Transition.Shape.CONICAL ); + nc.setForeRadius(1.0); + nc.setAftRadius(2.0); + nc.setMaterial( new Material.Bulk("test",density,true)); + + System.out.println( nc.getComponentVolume() ); + + double volume = Math.PI / 3.0 * (2.0*2.0 + 2.0 * 1.0 + 1.0 * 1.0) * 4.0; + + double mass = density * volume; + + System.out.println( volume ); + + assertEquals( volume, nc.getComponentVolume(), epsilonPercent * volume); + assertEquals( mass, nc.getMass(), epsilonPercent * mass ); + } + + @Test + public void simpleTransitionHollow1() { + Transition nc = new Transition(); + + final double epsilonPercent = 0.001; + final double density = 2.0; + + nc.setLength(1.0); + nc.setType( Transition.Shape.CONICAL ); + nc.setForeRadius(0.5); + nc.setAftRadius(1.0); + nc.setThickness(0.5); + nc.setMaterial( new Material.Bulk("test",density,true)); + + System.out.println( nc.getComponentVolume() ); + + // Volume of filled transition = + double filledVolume = Math.PI /3.0 * ( 1.0*1.0 + 1.0 * 0.5 + 0.5 * 0.5 ) * 1.0; + + // magic 2D cad drawing... + // + // Since the thickness >= fore radius, the + // hollowed out portion of the transition + // forms a cone. + // the dimensions of this cone were determined + // using a 2d cad tool. + + double innerConeRadius = 0.441; + double innerConeLength = 0.882; + double innerVolume = Math.PI /3.0 * innerConeLength * innerConeRadius * innerConeRadius; + + double volume = filledVolume - innerVolume; + + double mass = density * volume; + + System.out.println( volume ); + + assertEquals( volume, nc.getComponentVolume(), epsilonPercent * volume); + assertEquals( mass, nc.getMass(), epsilonPercent * mass ); + } + + @Test + public void simpleTransitionHollow2() { + Transition nc = new Transition(); + + final double epsilonPercent = 0.001; + final double density = 2.0; + + nc.setLength(1.0); + nc.setType( Transition.Shape.CONICAL ); + nc.setForeRadius(0.5); + nc.setAftRadius(1.0); + nc.setThickness(0.25); + nc.setMaterial( new Material.Bulk("test",density,true)); + + System.out.println( nc.getComponentVolume() ); + + // Volume of filled transition = + double filledVolume = Math.PI /3.0 * ( 1.0*1.0 + 1.0 * 0.5 + 0.5 * 0.5 ) * 1.0; + + // magic 2D cad drawing... + // + // Since the thickness < fore radius, the + // hollowed out portion of the transition + // forms a transition. + // the dimensions of this transition were determined + // using a 2d cad tool. + + double innerTransitionAftRadius = 0.7205; + double innerTransitionForeRadius = 0.2205; + double innerVolume = Math.PI /3.0 * ( innerTransitionAftRadius * innerTransitionAftRadius + innerTransitionAftRadius * innerTransitionForeRadius + innerTransitionForeRadius * innerTransitionForeRadius); + + double volume = filledVolume - innerVolume; + + double mass = density * volume; + + System.out.println( volume ); + + assertEquals( volume, nc.getComponentVolume(), epsilonPercent * volume); + assertEquals( mass, nc.getMass(), epsilonPercent * mass ); + } + +}