535 lines
18 KiB
Python
535 lines
18 KiB
Python
#***************************************************************************
|
|
#* *
|
|
#* Copyright (c) 2011, 2016 *
|
|
#* Jose Luis Cercos Pita <jlcercos@gmail.com> *
|
|
#* *
|
|
#* This program is free software; you can redistribute it and/or modify *
|
|
#* it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
#* as published by the Free Software Foundation; either version 2 of *
|
|
#* the License, or (at your option) any later version. *
|
|
#* for detail see the LICENCE text file. *
|
|
#* *
|
|
#* This program is distributed in the hope that it will be useful, *
|
|
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
#* GNU Library General Public License for more details. *
|
|
#* *
|
|
#* You should have received a copy of the GNU Library General Public *
|
|
#* License along with this program; if not, write to the Free Software *
|
|
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
#* USA *
|
|
#* *
|
|
#***************************************************************************
|
|
|
|
import math
|
|
import random
|
|
from FreeCAD import Vector, Rotation, Matrix, Placement
|
|
import Part
|
|
import Units
|
|
import FreeCAD as App
|
|
import FreeCADGui as Gui
|
|
from PySide import QtGui, QtCore
|
|
import Instance
|
|
from shipUtils import Math
|
|
import shipUtils.Units as USys
|
|
|
|
|
|
DENS = Units.parseQuantity("1025 kg/m^3") # Salt water
|
|
COMMON_BOOLEAN_ITERATIONS = 10
|
|
|
|
|
|
def placeShipShape(shape, draft, roll, trim):
|
|
"""Move the ship shape such that the free surface matches with the plane
|
|
z=0. The transformation will be applied on the input shape, so copy it
|
|
before calling this method if it should be preserved.
|
|
|
|
Position arguments:
|
|
shape -- Ship shape
|
|
draft -- Ship draft
|
|
roll -- Roll angle
|
|
trim -- Trim angle
|
|
|
|
Returned values:
|
|
shape -- The same transformed input shape. Just for debugging purposes, you
|
|
can discard it.
|
|
base_z -- The new base z coordinate (after applying the roll angle). Useful
|
|
if you want to revert back the transformation
|
|
"""
|
|
# Roll the ship. In order to can deal with large roll angles, we are
|
|
# proceeding as follows:
|
|
# 1.- Applying the roll with respect the base line
|
|
# 2.- Recentering the ship in the y direction
|
|
# 3.- Readjusting the base line
|
|
shape.rotate(Vector(0.0, 0.0, 0.0), Vector(1.0, 0.0, 0.0), roll)
|
|
base_z = shape.BoundBox.ZMin
|
|
shape.translate(Vector(0.0, draft * math.sin(math.radians(roll)), -base_z))
|
|
# Trim the ship. In this case we only need to correct the x direction
|
|
shape.rotate(Vector(0.0, 0.0, 0.0), Vector(0.0, -1.0, 0.0), trim)
|
|
shape.translate(Vector(draft * math.sin(math.radians(trim)), 0.0, 0.0))
|
|
shape.translate(Vector(0.0, 0.0, -draft))
|
|
|
|
return shape, base_z
|
|
|
|
|
|
def getUnderwaterSide(shape, force=True):
|
|
"""Get the underwater shape, simply cropping the provided shape by the z=0
|
|
free surface plane.
|
|
|
|
Position arguments:
|
|
shape -- Solid shape to be cropped
|
|
|
|
Keyword arguments:
|
|
force -- True if in case the common boolean operation fails, i.e. returns
|
|
no solids, the tool should retry it slightly moving the free surface. False
|
|
otherwise. (True by default)
|
|
|
|
Returned value:
|
|
Cropped shape. It is not modifying the input shape
|
|
"""
|
|
# Convert the shape into an active object
|
|
Part.show(shape)
|
|
orig = App.ActiveDocument.Objects[-1]
|
|
|
|
bbox = shape.BoundBox
|
|
xmin = bbox.XMin
|
|
xmax = bbox.XMax
|
|
ymin = bbox.YMin
|
|
ymax = bbox.YMax
|
|
zmin = bbox.ZMin
|
|
zmax = bbox.ZMax
|
|
|
|
# Create the "sea" box to intersect the ship
|
|
L = xmax - xmin
|
|
B = ymax - ymin
|
|
H = zmax - zmin
|
|
|
|
box = App.ActiveDocument.addObject("Part::Box","Box")
|
|
length_format = USys.getLengthFormat()
|
|
box.Placement = Placement(Vector(xmin - L, ymin - B, zmin - H),
|
|
Rotation(App.Vector(0,0,1),0))
|
|
box.Length = length_format.format(3.0 * L)
|
|
box.Width = length_format.format(3.0 * B)
|
|
box.Height = length_format.format(- zmin + H)
|
|
|
|
App.ActiveDocument.recompute()
|
|
common = App.activeDocument().addObject("Part::MultiCommon",
|
|
"UnderwaterSideHelper")
|
|
common.Shapes = [orig, box]
|
|
App.ActiveDocument.recompute()
|
|
if force and len(common.Shape.Solids) == 0:
|
|
# The common operation is failing, let's try moving a bit the free
|
|
# surface
|
|
msg = QtGui.QApplication.translate(
|
|
"ship_console",
|
|
"Boolean operation failed when trying to get the underwater side."
|
|
" The tool is retrying such operation slightly moving the free"
|
|
" surface position",
|
|
None)
|
|
App.Console.PrintWarning(msg + '\n')
|
|
random_bounds = 0.01 * H
|
|
i = 0
|
|
while len(common.Shape.Solids) == 0 and i < COMMON_BOOLEAN_ITERATIONS:
|
|
i += 1
|
|
box.Height = length_format.format(
|
|
- zmin + H + random.uniform(-random_bounds, random_bounds))
|
|
App.ActiveDocument.recompute()
|
|
|
|
out = common.Shape
|
|
App.ActiveDocument.removeObject(common.Name)
|
|
App.ActiveDocument.removeObject(orig.Name)
|
|
App.ActiveDocument.removeObject(box.Name)
|
|
App.ActiveDocument.recompute()
|
|
return out
|
|
|
|
|
|
def areas(ship, n, draft=None,
|
|
roll=Units.parseQuantity("0 deg"),
|
|
trim=Units.parseQuantity("0 deg")):
|
|
"""Compute the ship transversal areas
|
|
|
|
Position arguments:
|
|
ship -- Ship object (see createShip)
|
|
n -- Number of points to compute
|
|
|
|
Keyword arguments:
|
|
draft -- Ship draft (Design ship draft by default)
|
|
roll -- Roll angle (0 degrees by default)
|
|
trim -- Trim angle (0 degrees by default)
|
|
|
|
Returned value:
|
|
List of sections, each section contains 2 values, the x longitudinal
|
|
coordinate, and the transversal area. If n < 2, an empty list will be
|
|
returned.
|
|
"""
|
|
if n < 2:
|
|
return []
|
|
|
|
if draft is None:
|
|
draft = ship.Draft
|
|
|
|
shape, _ = placeShipShape(ship.Shape.copy(), draft, roll, trim)
|
|
shape = getUnderwaterSide(shape)
|
|
|
|
# Sections distance computation
|
|
bbox = shape.BoundBox
|
|
xmin = bbox.XMin
|
|
xmax = bbox.XMax
|
|
dx = (xmax - xmin) / (n - 1.0)
|
|
|
|
# Since we are computing the sections in the total length (not in the
|
|
# length between perpendiculars), we can grant that the starting and
|
|
# ending sections have null area
|
|
areas = [(Units.Quantity(xmin, Units.Length),
|
|
Units.Quantity(0.0, Units.Area))]
|
|
# And since we just need to compute areas we will create boxes with its
|
|
# front face at the desired transversal area position, computing the
|
|
# common solid part, dividing it by faces, and getting only the desired
|
|
# ones.
|
|
App.Console.PrintMessage("Computing transversal areas...\n")
|
|
App.Console.PrintMessage("Some Inventor representation errors can be"
|
|
" shown, please ignore them.\n")
|
|
for i in range(1, n - 1):
|
|
App.Console.PrintMessage("{0} / {1}\n".format(i, n - 2))
|
|
x = xmin + i * dx
|
|
try:
|
|
f = Part.Face(shape.slice(Vector(1,0,0), x))
|
|
except Part.OCCError:
|
|
msg = QtGui.QApplication.translate(
|
|
"ship_console",
|
|
"Part.OCCError: Transversal area computation failed",
|
|
None)
|
|
App.Console.PrintError(msg + '\n')
|
|
areas.append((Units.Quantity(x, Units.Length),
|
|
Units.Quantity(0.0, Units.Area)))
|
|
continue
|
|
# It is a valid face, so we can add this area
|
|
areas.append((Units.Quantity(x, Units.Length),
|
|
Units.Quantity(f.Area, Units.Area)))
|
|
# Last area is equal to zero (due to the total length usage)
|
|
areas.append((Units.Quantity(xmax, Units.Length),
|
|
Units.Quantity(0.0, Units.Area)))
|
|
App.Console.PrintMessage("Done!\n")
|
|
return areas
|
|
|
|
|
|
def displacement(ship, draft=None,
|
|
roll=Units.parseQuantity("0 deg"),
|
|
trim=Units.parseQuantity("0 deg")):
|
|
"""Compute the ship displacement
|
|
|
|
Position arguments:
|
|
ship -- Ship object (see createShip)
|
|
|
|
Keyword arguments:
|
|
draft -- Ship draft (Design ship draft by default)
|
|
roll -- Roll angle (0 degrees by default)
|
|
trim -- Trim angle (0 degrees by default)
|
|
|
|
Returned values:
|
|
disp -- The ship displacement (a density of the water of 1025 kg/m^3 is
|
|
assumed)
|
|
B -- Bouyance application point, i.e. Center of mass of the underwater side
|
|
Cb -- Block coefficient
|
|
|
|
The Bouyance center is referred to the original ship position.
|
|
"""
|
|
if draft is None:
|
|
draft = ship.Draft
|
|
|
|
shape, base_z = placeShipShape(ship.Shape.copy(), draft, roll, trim)
|
|
shape = getUnderwaterSide(shape)
|
|
|
|
vol = 0.0
|
|
cog = Vector()
|
|
if len(shape.Solids) > 0:
|
|
for solid in shape.Solids:
|
|
vol += solid.Volume
|
|
sCoG = solid.CenterOfMass
|
|
cog.x = cog.x + sCoG.x * solid.Volume
|
|
cog.y = cog.y + sCoG.y * solid.Volume
|
|
cog.z = cog.z + sCoG.z * solid.Volume
|
|
cog.x = cog.x / vol
|
|
cog.y = cog.y / vol
|
|
cog.z = cog.z / vol
|
|
|
|
bbox = shape.BoundBox
|
|
Vol = (bbox.XMax - bbox.XMin) * (bbox.YMax - bbox.YMin) * abs(bbox.ZMin)
|
|
|
|
# Undo the transformations on the bouyance point
|
|
B = Part.Point(Vector(cog.x, cog.y, cog.z))
|
|
m = Matrix()
|
|
m.move(Vector(0.0, 0.0, draft))
|
|
m.move(Vector(-draft * math.sin(trim.getValueAs("rad")), 0.0, 0.0))
|
|
m.rotateY(trim.getValueAs("rad"))
|
|
m.move(Vector(0.0,
|
|
-draft * math.sin(roll.getValueAs("rad")),
|
|
base_z))
|
|
m.rotateX(-roll.getValueAs("rad"))
|
|
B.transform(m)
|
|
|
|
try:
|
|
cb = vol / Vol
|
|
except ZeroDivisionError:
|
|
msg = QtGui.QApplication.translate(
|
|
"ship_console",
|
|
"ZeroDivisionError: Null volume found during the displacement"
|
|
" computation!",
|
|
None)
|
|
App.Console.PrintError(msg + '\n')
|
|
cb = 0.0
|
|
|
|
|
|
# Return the computed data
|
|
return (DENS * Units.Quantity(vol, Units.Volume),
|
|
Vector(B.X, B.Y, B.Z),
|
|
cb)
|
|
|
|
|
|
def wettedArea(shape, draft, roll=Units.parseQuantity("0 deg"),
|
|
trim=Units.parseQuantity("0 deg")):
|
|
"""Compute the ship wetted area
|
|
|
|
Position arguments:
|
|
shape -- External faces of the ship hull
|
|
draft -- Ship draft
|
|
|
|
Keyword arguments:
|
|
roll -- Roll angle (0 degrees by default)
|
|
trim -- Trim angle (0 degrees by default)
|
|
|
|
Returned value:
|
|
The wetted area, i.e. The underwater side area
|
|
"""
|
|
shape, _ = placeShipShape(shape.copy(), draft, roll, trim)
|
|
shape = getUnderwaterSide(shape, force=False)
|
|
|
|
area = 0.0
|
|
for f in shape.Faces:
|
|
area = area + f.Area
|
|
return Units.Quantity(area, Units.Area)
|
|
|
|
|
|
def moment(ship, draft=None,
|
|
roll=Units.parseQuantity("0 deg"),
|
|
trim=Units.parseQuantity("0 deg")):
|
|
"""Compute the moment required to trim the ship 1cm
|
|
|
|
Position arguments:
|
|
ship -- Ship object (see createShip)
|
|
|
|
Keyword arguments:
|
|
draft -- Ship draft (Design ship draft by default)
|
|
roll -- Roll angle (0 degrees by default)
|
|
trim -- Trim angle (0 degrees by default)
|
|
|
|
Returned value:
|
|
Moment required to trim the ship 1cm. Such moment is positive if it cause a
|
|
positive trim angle. The moment is expressed as a mass by a distance, not as
|
|
a force by a distance
|
|
"""
|
|
disp_orig, B_orig, _ = displacement(ship, draft, roll, trim)
|
|
xcb_orig = Units.Quantity(B_orig.x, Units.Length)
|
|
|
|
factor = 10.0
|
|
x = 0.5 * ship.Length.getValueAs('cm').Value
|
|
y = 1.0
|
|
angle = math.atan2(y, x) * Units.Radian
|
|
trim_new = trim + factor * angle
|
|
disp_new, B_new, _ = displacement(ship, draft, roll, trim_new)
|
|
xcb_new = Units.Quantity(B_new.x, Units.Length)
|
|
|
|
mom0 = -disp_orig * xcb_orig
|
|
mom1 = -disp_new * xcb_new
|
|
return (mom1 - mom0) / factor
|
|
|
|
|
|
def floatingArea(ship, draft=None,
|
|
roll=Units.parseQuantity("0 deg"),
|
|
trim=Units.parseQuantity("0 deg")):
|
|
"""Compute the ship floating area
|
|
|
|
Position arguments:
|
|
ship -- Ship object (see createShip)
|
|
|
|
Keyword arguments:
|
|
draft -- Ship draft (Design ship draft by default)
|
|
roll -- Roll angle (0 degrees by default)
|
|
trim -- Trim angle (0 degrees by default)
|
|
|
|
Returned values:
|
|
area -- Ship floating area
|
|
cf -- Floating area coefficient
|
|
"""
|
|
if draft is None:
|
|
draft = ship.Draft
|
|
|
|
# We want to intersect the whole ship with the free surface, so in this case
|
|
# we must not use the underwater side (or the tool will fail)
|
|
shape, _ = placeShipShape(ship.Shape.copy(), draft, roll, trim)
|
|
|
|
try:
|
|
f = Part.Face(shape.slice(Vector(0,0,1), 0.0))
|
|
area = Units.Quantity(f.Area, Units.Area)
|
|
except Part.OCCError:
|
|
msg = QtGui.QApplication.translate(
|
|
"ship_console",
|
|
"Part.OCCError: Floating area cannot be computed",
|
|
None)
|
|
App.Console.PrintError(msg + '\n')
|
|
area = Units.Quantity(0.0, Units.Area)
|
|
|
|
bbox = shape.BoundBox
|
|
Area = (bbox.XMax - bbox.XMin) * (bbox.YMax - bbox.YMin)
|
|
try:
|
|
cf = area.Value / Area
|
|
except ZeroDivisionError:
|
|
msg = QtGui.QApplication.translate(
|
|
"ship_console",
|
|
"ZeroDivisionError: Null area found during the floating area"
|
|
" computation!",
|
|
None)
|
|
App.Console.PrintError(msg + '\n')
|
|
cf = 0.0
|
|
|
|
return area, cf
|
|
|
|
|
|
def BMT(ship, draft=None, trim=Units.parseQuantity("0 deg")):
|
|
"""Calculate "ship Bouyance center" - "transversal metacenter" radius
|
|
|
|
Position arguments:
|
|
ship -- Ship object (see createShip)
|
|
|
|
Keyword arguments:
|
|
draft -- Ship draft (Design ship draft by default)
|
|
trim -- Trim angle (0 degrees by default)
|
|
|
|
Returned value:
|
|
BMT radius
|
|
"""
|
|
if draft is None:
|
|
draft = ship.Draft
|
|
|
|
roll = Units.parseQuantity("0 deg")
|
|
_, B0, _ = displacement(ship, draft, roll, trim)
|
|
|
|
|
|
nRoll = 2
|
|
maxRoll = Units.parseQuantity("7 deg")
|
|
|
|
BM = 0.0
|
|
for i in range(nRoll):
|
|
roll = (maxRoll / nRoll) * (i + 1)
|
|
_, B1, _ = displacement(ship, draft, roll, trim)
|
|
# * M
|
|
# / \
|
|
# / \ BM ==|> BM = (BB/2) / sin(alpha/2)
|
|
# / \
|
|
# *-------*
|
|
# BB
|
|
BB = B1 - B0
|
|
BB.x = 0.0
|
|
# nRoll is actually representing the weight function
|
|
BM += 0.5 * BB.Length / math.sin(math.radians(0.5 * roll)) / nRoll
|
|
return Units.Quantity(BM, Units.Length)
|
|
|
|
|
|
def mainFrameCoeff(ship, draft=None):
|
|
"""Compute the main frame coefficient
|
|
|
|
Position arguments:
|
|
ship -- Ship object (see createShip)
|
|
|
|
Keyword arguments:
|
|
draft -- Ship draft (Design ship draft by default)
|
|
|
|
Returned value:
|
|
Ship main frame area coefficient
|
|
"""
|
|
if draft is None:
|
|
draft = ship.Draft
|
|
|
|
shape, _ = placeShipShape(ship.Shape.copy(), draft,
|
|
Units.parseQuantity("0 deg"),
|
|
Units.parseQuantity("0 deg"))
|
|
shape = getUnderwaterSide(shape)
|
|
|
|
try:
|
|
f = Part.Face(shape.slice(Vector(1,0,0), 0.0))
|
|
area = f.Area
|
|
except Part.OCCError:
|
|
msg = QtGui.QApplication.translate(
|
|
"ship_console",
|
|
"Part.OCCError: Main frame area cannot be computed",
|
|
None)
|
|
App.Console.PrintError(msg + '\n')
|
|
area = 0.0
|
|
|
|
bbox = shape.BoundBox
|
|
Area = (bbox.YMax - bbox.YMin) * (bbox.ZMax - bbox.ZMin)
|
|
|
|
try:
|
|
cm = area / Area
|
|
except ZeroDivisionError:
|
|
msg = QtGui.QApplication.translate(
|
|
"ship_console",
|
|
"ZeroDivisionError: Null area found during the main frame area"
|
|
" coefficient computation!",
|
|
None)
|
|
App.Console.PrintError(msg + '\n')
|
|
cm = 0.0
|
|
|
|
return cm
|
|
|
|
|
|
class Point:
|
|
"""Hydrostatics point, that contains the following members:
|
|
|
|
draft -- Ship draft
|
|
trim -- Ship trim
|
|
disp -- Ship displacement
|
|
xcb -- Bouyance center X coordinate
|
|
wet -- Wetted ship area
|
|
mom -- Triming 1cm ship moment
|
|
farea -- Floating area
|
|
KBt -- Transversal KB height
|
|
BMt -- Transversal BM height
|
|
Cb -- Block coefficient.
|
|
Cf -- Floating coefficient.
|
|
Cm -- Main frame coefficient.
|
|
|
|
The moment to trim the ship 1 cm is positive when is resulting in a positive
|
|
trim angle.
|
|
"""
|
|
def __init__(self, ship, faces, draft, trim):
|
|
"""Compute all the hydrostatics.
|
|
|
|
Position argument:
|
|
ship -- Ship instance
|
|
faces -- Ship external faces
|
|
draft -- Ship draft
|
|
trim -- Trim angle
|
|
"""
|
|
disp, B, cb = displacement(ship, draft=draft, trim=trim)
|
|
if not faces:
|
|
wet = 0.0
|
|
else:
|
|
wet = wettedArea(faces, draft=draft, trim=trim)
|
|
mom = moment(ship, draft=draft, trim=trim)
|
|
farea, cf = floatingArea(ship, draft=draft, trim=trim)
|
|
bm = BMT(ship, draft=draft, trim=trim)
|
|
cm = mainFrameCoeff(ship, draft=draft)
|
|
# Store final data
|
|
self.draft = draft
|
|
self.trim = trim
|
|
self.disp = disp
|
|
self.xcb = Units.Quantity(B.x, Units.Length)
|
|
self.wet = wet
|
|
self.farea = farea
|
|
self.mom = mom
|
|
self.KBt = Units.Quantity(B.z, Units.Length)
|
|
self.BMt = bm
|
|
self.Cb = cb
|
|
self.Cf = cf
|
|
self.Cm = cm
|