Module voxelfuse.simulation
Simulation Class
Initialized from a VoxelModel object. Used to configure VoxCad and Voxelyze simulations.
Copyright 2020 - Cole Brauer, Dan Aukes
Expand source code
"""
Simulation Class
Initialized from a VoxelModel object. Used to configure VoxCad and Voxelyze simulations.
----
Copyright 2020 - Cole Brauer, Dan Aukes
"""
import os
import time
import subprocess
import multiprocessing
import numpy as np
from enum import Enum
from datetime import date
from typing import List, Tuple, TextIO
from tqdm import tqdm
from voxelfuse.voxel_model import VoxelModel, writeHeader, writeData, writeOpen, writeClos
from voxelfuse.primitives import empty
# Floating point error threshold for rounding to zero
FLOATING_ERROR = 0.0000000001
class Axis(Enum):
"""
Options for axes and planes.
"""
NONE = -1
X = 0
Y = 1
Z = 2
class StopCondition(Enum):
"""
Options for simulation stop conditions.
"""
NONE = 0
TIME_STEP = 1
TIME_VALUE = 2
TEMP_CYCLES = 3
ENERGY_CONST = 4
ENERGY_KFLOOR = 5
MOTION_FLOOR = 6
class BCShape(Enum):
"""
Options for simulation boundary condition shapes.
"""
BOX = 0
CYLINDER = 1
SPHERE = 2
class _BCRegion:
"""
Internal boundary condition class.
"""
def __init__(self, name: str,
shape: BCShape,
position: Tuple[float, float, float],
size: Tuple[float, float, float],
radius: float,
fixed_dof: int,
force: Tuple[float, float, float],
displacement: Tuple[float, float, float],
torque: Tuple[float, float, float],
angular_displacement: Tuple[float, float, float]):
self.name = name
self.shape = shape
self.position = position
self.size = size
self.radius = radius
self.color = (0.6, 0.4, 0.4, 0.5)
self.fixed_dof = fixed_dof
self.force = force
self.displacement = displacement
self.torque = torque
self.angular_displacement = angular_displacement
class _Sensor:
"""
Internal sensor class.
"""
def __init__(self, name: str, coords: Tuple[int, int, int], axis: Axis):
self.name = name
self.coords = coords
self.axis = axis
class _Keyframe:
"""
Internal keyframe class.
"""
def __init__(self, time_value: float,
amplitude_pos: float,
amplitude_neg: float,
percent_pos: float,
period: float,
phase_offset: float,
temp_offset: float,
const_temp: bool,
square_wave: bool):
self.time_value = time_value
self.amplitude_pos = amplitude_pos
self.amplitude_neg = amplitude_neg
self.percent_pos = percent_pos
self.period = period
self.phase_offset = phase_offset
self.temp_offset = temp_offset
self.const_temp = const_temp
self.square_wave = square_wave
class _TempControlGroup:
"""
Internal temperature control group class.
"""
def __init__(self, name: str, locations: List[Tuple[int, int, int]], keyframes: List[_Keyframe]):
self.name = name
self.locations = locations
self.keyframes = keyframes
class _Disconnection:
"""
Internal disconnection class.
"""
def __init__(self, voxel_1: Tuple[int, int, int], voxel_2: Tuple[int, int, int]):
self.vx1 = voxel_1
self.vx2 = voxel_2
class Simulation:
"""
Simulation object that stores a VoxelModel and its associated simulation settings.
"""
def __init__(self, voxel_model, id_number: int = 0):
"""
Initialize a Simulation object with default settings.
Models located at positive coordinate values will have their workspace
size adjusted to maintain their position in the exported simulation.
Models located at negative coordinate values will be shifted to the origin.
Args:
voxel_model: VoxelModel
"""
# Simulation ID and start date
self.id = id_number
self.date = date.today()
# Fit workspace and union with an empty object at the origin to clear offsets if object is raised
self.__model = ((VoxelModel.copy(voxel_model).fitWorkspace()) | empty(num_materials=(voxel_model.materials.shape[1] - 1), resolution=voxel_model.resolution)).removeDuplicateMaterials()
# Simulator ##############
# Integration
self.__integrator = 0
self.__dtFraction = 1.0
# Damping
self.__dampingBond = 1.0 # (0-1) Bulk material damping
self.__dampingEnvironment = 0.0001 # (0-0.1) Damping caused by fluid environment
# Collisions
self.__collisionEnable = False
self.__collisionDamping = 1.0 # (0-2) Elastic vs inelastic conditions
self.__collisionSystem = 3
self.__collisionHorizon = 3
# Features
self.__blendingEnable = False
self.__xMixRadius = 0
self.__yMixRadius = 0
self.__zMixRadius = 0
self.__blendingModel = 0
self.__polyExp = 1
self.__volumeEffectsEnable = False
self.__hydrogelModelEnable = False
# Stop conditions
self.__stopConditionType = StopCondition.NONE
self.__stopConditionValue = 0.0
# Equilibrium mode
self.__equilibriumModeEnable = False
# Environment ############
# Boundary conditions
self.__bcRegions = []
# Gravity
self.__gravityEnable = True
self.__gravityValue = -9.81
self.__floorEnable = True
# Thermal
self.__temperatureEnable = False
self.__temperatureBaseValue = 25.0
self.__temperatureVaryEnable = False
self.__temperatureVaryAmplitude = 0.0
self.__temperatureVaryPeriod = 0.0
self.__temperatureVaryOffset = 0.0
self.__growthAmplitude = 0.0
# Sensors #######
self.__sensors = []
# Temperature Controls #######
self.__currentTempControlGroup = 0
self.__localTempControls = []
# Disconnected Bonds #######
self.__disconnections = []
# Results ################
self.results = []
self.valueMap = np.zeros_like(voxel_model.voxels, dtype=np.float32)
@classmethod
def copy(cls, simulation):
"""
Create new Simulation object with the same settings as an existing Simulation object.
Args:
simulation: Simulation to copy
Returns:
Simulation
"""
# Create new simulation object and copy attribute values
new_simulation = cls(simulation.__model)
new_simulation.__dict__ = simulation.__dict__.copy()
# Update ID and date
new_simulation.id = simulation.id + 1
new_simulation.date = date.today()
# Make lists copies instead of references
new_simulation.__bcRegions = simulation.__bcRegions.copy()
new_simulation.__sensors = simulation.__sensors.copy()
new_simulation.__localTempControls = simulation.__localTempControls.copy()
new_simulation.__disconnections = simulation.__disconnections.copy()
new_simulation.results = simulation.results.copy()
new_simulation.valueMap = simulation.valueMap.copy()
return new_simulation
# Configure settings ##################################
def setModel(self, voxel_model):
"""
Set the model for a simulation.
Models located at positive coordinate values will have their workspace
size adjusted to maintain their position in the exported simulation.
Models located at negative coordinate values will be shifted to the origin.
Args:
voxel_model: VoxelModel
Returns:
None
"""
# Fit workspace and union with an empty object at the origin to clear offsets if object is raised
self.__model = ((VoxelModel.copy(voxel_model).fitWorkspace()) | empty(num_materials=(voxel_model.materials.shape[1] - 1))).removeDuplicateMaterials()
def setDamping(self, bond: float = 1.0, environment: float = 0.0001):
"""
Set simulation damping parameters.
Environment damping can be used to simulate fluid environments. 0 represents
a vacuum and larger values represent a viscous fluid.
Args:
bond: Voxel bond damping (0-1)
environment: Environment damping (0-0.1)
Returns:
None
"""
self.__dampingBond = bond
self.__dampingEnvironment = environment
def setCollision(self, enable: bool = True, damping: float = 1.0):
"""
Set simulation collision parameters.
A damping value of 0 represents completely elastic collisions and
higher values represent inelastic collisions.
Args:
enable: Enable/disable collisions
damping: Collision damping (0-2)
Returns:
None
"""
self.__collisionEnable = enable
self.__collisionDamping = damping
def setHydrogelModel(self, enable: bool = True):
"""
Set hydrogel model parameters.
Args:
enable: Enable/disable hydrogel model
Returns:
None
"""
self.__hydrogelModelEnable = enable
def setStopCondition(self, condition: StopCondition = StopCondition.NONE, value: float = 0):
"""
Set simulation stop condition.
Args:
condition: Stop condition type, set using StopCondition class
value: Stop condition value
Returns:
None
"""
self.__stopConditionType = condition
self.__stopConditionValue = value
def setEquilibriumMode(self, enable: bool = True):
"""
Set simulation equilibrium mode.
Args:
enable: Enable/disable equilibrium mode
Returns:
None
"""
self.__equilibriumModeEnable = enable
def setGravity(self, enable: bool = True, value: float = -9.81, enable_floor: bool = True):
"""
Set simulation gravity parameters.
Args:
enable: Enable/disable gravity
value: Acceleration due to gravity in m/sec^2
enable_floor: Enable/disable ground plane
Returns:
None
"""
self.__gravityEnable = enable
self.__gravityValue = value
self.__floorEnable = enable_floor
def setFixedThermal(self, enable: bool = True, base_temp: float = 25.0, growth_amplitude: float = 0.0):
"""
Set a fixed environment temperature.
Args:
enable: Enable/disable temperature
base_temp: Temperature in degrees C
growth_amplitude: Set to 1 to enable expansion from base size
Returns:
None
"""
self.__temperatureEnable = enable
self.__temperatureBaseValue = base_temp
self.__temperatureVaryEnable = False
self.__growthAmplitude = growth_amplitude
def setVaryingThermal(self, enable: bool = True, base_temp: float = 25.0, amplitude: float = 0.0, period: float = 1.0, offset: float = 0.0, growth_amplitude: float = 1.0):
"""
Set a varying environment temperature.
Args:
enable: Enable/disable temperature
base_temp: Base temperature in degrees C
amplitude: Temperature fluctuation amplitude
period: Temperature fluctuation period
offset: Temperature offset (not currently supported)
growth_amplitude: Set to 1 to enable expansion from base size
Returns:
None
"""
self.__temperatureEnable = enable
self.__temperatureBaseValue = base_temp
self.__temperatureVaryEnable = enable
self.__temperatureVaryAmplitude = amplitude
self.__temperatureVaryPeriod = period
self.__temperatureVaryOffset = offset
self.__growthAmplitude = growth_amplitude
# Read settings ##################################
def getModel(self):
"""
Get the simulation model.
Returns:
VoxelModel
"""
return self.__model
def getVoxelDim(self):
"""
Get the side dimension of a voxel in mm.
Returns:
Float
"""
res = self.__model.resolution
return (1.0/res) * 0.001
def getDamping(self):
"""
Get simulation damping parameters.
Returns:
Voxel bond damping, Environment damping
"""
return self.__dampingBond, self.__dampingEnvironment
def getCollision(self):
"""
Get simulation collision parameters.
Returns:
Enable/disable collisions, Collision damping
"""
return self.__collisionEnable, self.__collisionDamping
def getHydrogelModel(self):
"""
Get hydrogel model parameters.
Returns:
Enable/disable hydrogel model
"""
return self.__hydrogelModelEnable
def getStopCondition(self):
"""
Get simulation stop condition.
Returns:
Stop condition type, Stop condition value
"""
return self.__stopConditionType, self.__stopConditionValue
def getEquilibriumMode(self):
"""
Get simulation equilibrium mode.
Returns:
Enable/disable equilibrium mode
"""
return self.__equilibriumModeEnable
def getGravity(self):
"""
Get simulation gravity parameters.
Returns:
Enable/disable gravity, Acceleration due to gravity in m/sec^2, Enable/disable ground plane
"""
return self.__gravityEnable, self.__gravityValue, self.__floorEnable
def getThermal(self):
"""
Get simulation temperature parameters.
Returns:
Enable/disable temperature, Base temperature in degrees C, Enable/disable temperature fluctuation, Temperature fluctuation amplitude, Temperature fluctuation period, Temperature offset
"""
return self.__temperatureEnable, self.__temperatureBaseValue, self.__temperatureVaryEnable, self.__temperatureVaryAmplitude, self.__temperatureVaryPeriod, self.__temperatureVaryOffset
# Add forces, constraints, and sensors ##################################
# Boundary condition sizes and positions are expressed as percentages of the overall model size
# radius is a percentage of the largest model dimension
# Fixed DOF bits correspond to: Rz, Ry, Rx, Z, Y, X
# 0: Free, force will be applied
# 1: Fixed, displacement will be applied
# Displacement is expressed in mm
def clearBoundaryConditions(self):
"""
Remove all boundary conditions from a Simulation object.
Returns:
None
"""
self.__bcRegions = []
# self.__bcVoxels = []
# Default box boundary condition is a fixed constraint in the YZ plane
def addBoundaryConditionVoxel(self, position: Tuple[int, int, int] = (0, 0, 0),
fixed_dof: int = 0b111111,
force: Tuple[float, float, float] = (0, 0, 0),
displacement: Tuple[float, float, float] = (0, 0, 0),
torque: Tuple[float, float, float] = (0, 0, 0),
angular_displacement: Tuple[float, float, float] = (0, 0, 0),
name: str = None):
"""
Add a boundary condition at a specific voxel.
The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits
correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque
will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be
applied.
Args:
position: Position in voxels
fixed_dof: Fixed degrees of freedom
force: Force vector in N
displacement: Displacement vector in mm
torque: Torque values in Nm
angular_displacement: Angular displacement values in deg
name: Boundary condition name
Returns:
None
"""
x = position[0] - self.__model.coords[0]
y = position[1] - self.__model.coords[1]
z = position[2] - self.__model.coords[2]
x_len = int(self.__model.voxels.shape[0])
y_len = int(self.__model.voxels.shape[1])
z_len = int(self.__model.voxels.shape[2])
pos = ((x+0.5)/x_len, (y+0.5)/y_len, (z+0.5)/z_len)
radius = 0.49/x_len
bc = _BCRegion(name, BCShape.SPHERE, pos, (0.0, 0.0, 0.0), radius, fixed_dof, force, displacement, torque, angular_displacement)
self.__bcRegions.append(bc)
# Default box boundary condition is a fixed constraint in the XY plane (bottom layer)
def addBoundaryConditionBox(self, position: Tuple[float, float, float] = (0.0, 0.0, 0.0),
size: Tuple[float, float, float] = (1.0, 1.0, 0.01),
fixed_dof: int = 0b111111,
force: Tuple[float, float, float] = (0, 0, 0),
displacement: Tuple[float, float, float] = (0, 0, 0),
torque: Tuple[float, float, float] = (0, 0, 0),
angular_displacement: Tuple[float, float, float] = (0, 0, 0),
name: str = None):
"""
Add a box-shaped boundary condition.
Boundary condition position and size are expressed as percentages of the
overall model size. The fixed DOF value should be set as a 6-bit binary
value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X.
If a bit is set to 0, the corresponding force/torque will be applied. If
a bit is set to 1, the DOF will be fixed and the displacement will be
applied.
Args:
position: Box corner position (0-1)
size: Box size (0-1)
fixed_dof: Fixed degrees of freedom
force: Force vector in N
displacement: Displacement vector in mm
torque: Torque values in Nm
angular_displacement: Angular displacement values in deg
name: Boundary condition name
Returns:
None
"""
bc = _BCRegion(name, BCShape.BOX, position, size, 0, fixed_dof, force, displacement, torque, angular_displacement)
self.__bcRegions.append(bc)
# Default sphere boundary condition is a fixed constraint centered in the model
def addBoundaryConditionSphere(self, position: Tuple[float, float, float] = (0.5, 0.5, 0.5),
radius: float = 0.05,
fixed_dof: int = 0b111111,
force: Tuple[float, float, float] = (0, 0, 0),
displacement: Tuple[float, float, float] = (0, 0, 0),
torque: Tuple[float, float, float] = (0, 0, 0),
angular_displacement: Tuple[float, float, float] = (0, 0, 0),
name: str = None):
"""
Add a spherical boundary condition.
Boundary condition position and radius are expressed as percentages of the
overall model size. The fixed DOF value should be set as a 6-bit binary
value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X.
If a bit is set to 0, the corresponding force/torque will be applied. If
a bit is set to 1, the DOF will be fixed and the displacement will be
applied.
Args:
position: Sphere center position (0-1)
radius: Sphere radius (0-1)
fixed_dof: Fixed degrees of freedom
force: Force vector in N
displacement: Displacement vector in mm
torque: Torque values in Nm
angular_displacement: Angular displacement values in deg
name: Boundary condition name
Returns:
None
"""
bc = _BCRegion(name, BCShape.SPHERE, position, (0.0, 0.0, 0.0), radius, fixed_dof, force, displacement, torque, angular_displacement)
self.__bcRegions.append(bc)
# Default cylinder boundary condition is a fixed constraint centered in the model
def addBoundaryConditionCylinder(self, position: Tuple[float, float, float] = (0.45, 0.5, 0.5), axis: Axis = Axis.X,
height: float = 0.1,
radius: float = 0.05,
fixed_dof: int = 0b111111,
force: Tuple[float, float, float] = (0, 0, 0),
displacement: Tuple[float, float, float] = (0, 0, 0),
torque: Tuple[float, float, float] = (0, 0, 0),
angular_displacement: Tuple[float, float, float] = (0, 0, 0),
name: str = None):
"""
Add a cylindrical boundary condition.
Boundary condition position and size are expressed as percentages of the
overall model size. The fixed DOF value should be set as a 6-bit binary
value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X.
If a bit is set to 0, the corresponding force/torque will be applied. If
a bit is set to 1, the DOF will be fixed and the displacement will be
applied.
Args:
position: Boundary condition origin position (0-1)
axis: Cylinder axis (0-2)
height: Cylinder height (0-1)
radius: Cylinder radius (0-1)
fixed_dof: Fixed degrees of freedom
force: Force vector in N
displacement: Displacement vector in mm
torque: Torque values in Nm
angular_displacement: Angular displacement values in deg
name: Boundary condition name
Returns:
None
"""
size = [0.0, 0.0, 0.0]
size[axis.value] = height
bc = _BCRegion(name, BCShape.CYLINDER, position, (size[0], size[1], size[2]), radius, fixed_dof, force, displacement, torque, angular_displacement)
self.__bcRegions.append(bc)
def clearSensors(self):
"""
Remove all sensors from a Simulation object.
Returns:
None
"""
self.__sensors = []
def addSensor(self, location: Tuple[int, int, int] = (0, 0, 0), axis: Axis = Axis.NONE, name: str = None): # TODO: Make Voxelyze use axis parameter
"""
Add a sensor to a voxel.
Args:
location: Sensor location in voxels
axis: Sensor measurement axis
name: Sensor name
Returns:
None
"""
x = location[0] - self.__model.coords[0]
y = location[1] - self.__model.coords[1]
z = location[2] - self.__model.coords[2]
sensor = _Sensor(name, (x, y, z), axis)
self.__sensors.append(sensor)
if not self.__model.isOccupied((x, y, z)):
print('WARNING: No material present at sensor voxel ' + str((x, y, z)))
def clearDisconnections(self):
"""
Clear all disconnected voxel bonds.
Returns:
None
"""
self.__disconnections = []
def addDisconnection(self, voxel_1: Tuple[int, int, int], voxel_2: Tuple[int, int, int]):
"""
Specify a pair of voxels which should be disconnected
Args:
voxel_1: Coordinates in voxels
voxel_2: Coordinates in voxels
Returns:
None
"""
dc = _Disconnection(voxel_1, voxel_2)
self.__disconnections.append(dc)
def addTempControlGroup(self, locations: List[Tuple[int, int, int]] = None, name: str = None):
"""
Add a new temperature control group and select it.
Args:
locations: Control element locations in voxels as a list of tuples
name: Group name
Returns:
None
"""
# Enable temperature control
self.__temperatureEnable = True
self.__temperatureVaryEnable = True
if locations is None:
print('No locations provided - applying temperature control group to entire model')
x_len = self.__model.voxels.shape[0]
y_len = self.__model.voxels.shape[1]
z_len = self.__model.voxels.shape[2]
locations = []
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
locations.append((x, y, z))
group = _TempControlGroup(name, locations, [])
self.__localTempControls.append(group)
self.__currentTempControlGroup = len(self.__localTempControls)-1
def removeTempControlGroup(self, index: int = 0):
"""
Remove a temperature control group by index.
Args:
index: Temperature control group index
Returns:
Name of removed group
"""
group = self.__localTempControls.pop(index)
self.__currentTempControlGroup = len(self.__localTempControls)-1
return group.name
def selectTempControlGroup(self, index: int = 0):
"""
Select which keyframe new temperature control elements should be added to.
Args:
index: Temperature control group index
Returns:
None
"""
self.__currentTempControlGroup = index
def clearTempControlGroups(self):
"""
Clear all temperature control groups.
Returns:
None
"""
self.__currentTempControlGroup = 0
self.__localTempControls = []
def cleanTempControlGroups(self):
"""
Remove invalid temperature control groups.
Invalid groups have no keyframes, or have no target voxels.
Returns:
Number of groups removed
"""
removed_groups = []
for g in range(len(self.__localTempControls)):
g = g - len(removed_groups)
group = self.__localTempControls[g]
if len(group.keyframes) == 0 or len(group.locations) == 0:
removed_groups.append(self.removeTempControlGroup(g))
self.__currentTempControlGroup = len(self.__localTempControls)-1
named_groups = []
for n in removed_groups:
if n is not None:
named_groups.append(n)
if len(removed_groups) == 0:
pass
elif len(named_groups) == 0:
print('Removed ' + str(len(removed_groups)) + ' temperature control groups')
else:
print('Removed ' + str(len(removed_groups)) + ' temperature control groups, including: ' + str(named_groups))
return len(removed_groups)
def clearKeyframes(self):
"""
Remove all keyframes assigned to the current temperature control group.
Returns:
None
"""
g = self.__currentTempControlGroup
self.__localTempControls[g].keyframes = []
def addKeyframe(self, time_value: float = 0, amplitude_pos: float = 0, amplitude_neg: float = -1, percent_pos: float = 0.5,
period: float = 1.0, phase_offset: float = 0, temp_offset: float = 0, const_temp: bool = False, square_wave: bool = False):
"""
Add a keyframe to a temperature control group.
Args:
time_value: Time at which keyframe should take effect (sec)
amplitude_pos: Control element positive temperature amplitude (deg C)
amplitude_neg: Control element negative temperature amplitude (deg C)
percent_pos: Percent of period spanned by positive temperature amplitude (0-1)
period: Period of the control signal (sec)
phase_offset: Control element phase offset for time-varying thermal (rad)
temp_offset: Control element temperature offset for time-varying thermal (deg C)
const_temp: Enable/disable setting a constant target temperature that respects heating/cooling rates
square_wave: Enable/disable converting signal to a square wave (positive -> a = amplitude1, negative -> a = 0)
Returns:
None
"""
amplitude_pos = max(amplitude_pos, 0) # amplitude_pos must be positive
if amplitude_neg < 0: # If amplitude neg is not given (or not negative) use amplitude_pos instead
amplitude_neg = amplitude_pos
g = self.__currentTempControlGroup
kf = _Keyframe(time_value, amplitude_pos, amplitude_neg, percent_pos, period, phase_offset, temp_offset, const_temp, square_wave)
self.__localTempControls[g].keyframes.append(kf)
def initializeTempMap(self):
"""
Initialize temperature control groups to which keyframes will be stored when using applyTempMap.
Returns:
None
"""
# Clear any existing temperature controls
self.clearTempControlGroups()
# Get map size
x_len = self.__model.voxels.shape[0]
y_len = self.__model.voxels.shape[1]
z_len = self.__model.voxels.shape[2]
# Generate empty control element for each voxel
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
self.addTempControlGroup([(x, y, z)])
def applyTempMap(self, time_value, amp_pos_map, amp_neg_map=None, percent_pos_map=None, period_map=None,
phase_map=None, offset_map=None, const_temp_map=None, square_wave_map=None):
"""
Set the simulation temperature control elements based on a value maps of target temperature settings.
This function relies on the temperature control groups being in a specific order. initializeTempMap should be called prior to running this function.
Args:
time_value: Time at which keyframe should take effect (sec)
amp_pos_map: Array of target positive temperature amplitudes for each voxel (deg C)
amp_neg_map: Array of target negative temperature amplitudes for each voxel (deg C)
percent_pos_map: Array containing percent of period spanned by positive temperature amplitude for each voxel
period_map: Array of signal periods for each voxel (sec)
phase_map: Array of phase offsets for each voxel (rad)
offset_map: Array of temperature offsets for each voxel (deg C)
const_temp_map: Array of boolean values to enable/disable constant temperature mode
square_wave_map: Array of boolean values to enable/disable square wave mode
Returns:
None
"""
# Get map size
x_len = amp_pos_map.shape[0]
y_len = amp_pos_map.shape[1]
z_len = amp_pos_map.shape[2]
# Generate the control element for each voxel
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
if abs(amp_pos_map[x, y, z]) > FLOATING_ERROR or ((amp_neg_map is not None) and (abs(amp_neg_map[x, y, z]) > FLOATING_ERROR)): # If voxel is not empty
amplitude_pos = amp_pos_map[x, y, z]
if amp_neg_map is None:
amplitude_neg = amp_pos_map[x, y, z]
else:
amplitude_neg = amp_neg_map[x, y, z]
if percent_pos_map is None:
percent_pos = 0.5
else:
percent_pos = percent_pos_map[x, y, z]
if period_map is None:
period = 1.0
else:
period = period_map[x, y, z]
if phase_map is None:
phase_offset = 0
else:
phase_offset = phase_map[x, y, z]
if offset_map is None:
temp_offset = 0
else:
temp_offset = offset_map[x, y, z]
if const_temp_map is None:
const_temp = False
else:
const_temp = const_temp_map[x, y, z]
if square_wave_map is None:
square_wave = False
else:
square_wave = square_wave_map[x, y, z]
kf = _Keyframe(time_value, amplitude_pos, amplitude_neg, percent_pos, period, phase_offset, temp_offset, const_temp, square_wave)
g = z + y*z_len + x*y_len*z_len
self.__localTempControls[g].keyframes.append(kf)
# Export simulation ##################################
# Export simulation object to .vxa file for import into VoxCad or Voxelyze
def saveVXA(self, filename: str, compression: bool = False):
"""
Save model data to a .vxa file
The VoxCad simulation file format stores all the data contained in
a .vxc file (geometry, material palette) plus the simulation setup (simulation
parameters, environment settings, boundary conditions).
This format supports compression for the voxel data. Enabling compression allows
for larger models, but it may introduce geometry errors that particularly affect
small models.
The .vxa file type can be opened using VoxCad simulation software. However, it
cannot currently be reopened by a VoxelFuse script.
Args:
filename: File name
compression: Enable/disable voxel data compression
Returns:
None
"""
self.cleanTempControlGroups()
f = open(filename + '.vxa', 'w+')
print('Saving file: ' + f.name)
writeHeader(f, '1.0', 'ISO-8859-1')
writeOpen(f, 'VXA Version="' + str(1.1) + '"', 0)
self.writeSimData(f)
self.writeEnvironmentData(f)
self.writeSensors(f)
self.writeTempControls(f)
self.writeDisconnections(f)
self.__model.writeVXCData(f, compression)
writeClos(f, 'VXA', 0)
f.close()
# Write simulator settings to file
def writeSimData(self, f: TextIO):
"""
Write simulation parameters to a text file using the .vxa format.
Args:
f: File to write to
Returns:
None
"""
# Simulator settings
writeOpen(f, 'Simulator', 0)
writeOpen(f, 'Integration', 1)
writeData(f, 'Integrator', self.__integrator, 2)
writeData(f, 'DtFrac', self.__dtFraction, 2)
writeClos(f, 'Integration', 1)
writeOpen(f, 'Damping', 1)
writeData(f, 'BondDampingZ', self.__dampingBond, 2)
writeData(f, 'ColDampingZ', self.__collisionDamping, 2)
writeData(f, 'SlowDampingZ', self.__dampingEnvironment, 2)
writeClos(f, 'Damping', 1)
writeOpen(f, 'Collisions', 1)
writeData(f, 'SelfColEnabled', int(self.__collisionEnable), 2)
writeData(f, 'ColSystem', self.__collisionSystem, 2)
writeData(f, 'CollisionHorizon', self.__collisionHorizon, 2)
writeClos(f, 'Collisions', 1)
writeOpen(f, 'Features', 1)
writeData(f, 'BlendingEnabled', int(self.__blendingEnable), 2)
writeData(f, 'XMixRadius', self.__xMixRadius, 2)
writeData(f, 'YMixRadius', self.__yMixRadius, 2)
writeData(f, 'ZMixRadius', self.__zMixRadius, 2)
writeData(f, 'BlendModel', self.__blendingModel, 2)
writeData(f, 'PolyExp', self.__polyExp, 2)
writeData(f, 'VolumeEffectsEnabled', int(self.__volumeEffectsEnable), 2)
writeClos(f, 'Features', 1)
writeOpen(f, 'StopCondition', 1)
writeData(f, 'StopConditionType', self.__stopConditionType.value, 2)
writeData(f, 'StopConditionValue', self.__stopConditionValue, 2)
writeClos(f, 'StopCondition', 1)
writeOpen(f, 'EquilibriumMode', 1)
writeData(f, 'EquilibriumModeEnabled', int(self.__equilibriumModeEnable), 2)
writeClos(f, 'EquilibriumMode', 1)
writeClos(f, 'Simulator', 0)
# Write environment settings to file
def writeEnvironmentData(self, f: TextIO):
"""
Write simulation environment parameters to a text file using the .vxa format.
Args:
f: File to write to
Returns:
None
"""
# Environment settings
writeOpen(f, 'Environment', 0)
writeOpen(f, 'Boundary_Conditions', 1)
writeData(f, 'NumBCs', len(self.__bcRegions), 2)
for bc in self.__bcRegions:
writeOpen(f, 'FRegion', 2)
if bc.name is not None:
writeData(f, 'Name', bc.name, 3)
writeData(f, 'PrimType', int(bc.shape.value), 3)
writeData(f, 'X', bc.position[0], 3)
writeData(f, 'Y', bc.position[1], 3)
writeData(f, 'Z', bc.position[2], 3)
writeData(f, 'dX', bc.size[0], 3)
writeData(f, 'dY', bc.size[1], 3)
writeData(f, 'dZ', bc.size[2], 3)
writeData(f, 'Radius', bc.radius, 3)
writeData(f, 'R', bc.color[0], 3)
writeData(f, 'G', bc.color[1], 3)
writeData(f, 'B', bc.color[2], 3)
writeData(f, 'alpha', bc.color[3], 3)
writeData(f, 'DofFixed', bc.fixed_dof, 3)
writeData(f, 'ForceX', bc.force[0], 3)
writeData(f, 'ForceY', bc.force[1], 3)
writeData(f, 'ForceZ', bc.force[2], 3)
writeData(f, 'TorqueX', bc.torque[0], 3)
writeData(f, 'TorqueY', bc.torque[1], 3)
writeData(f, 'TorqueZ', bc.torque[2], 3)
writeData(f, 'DisplaceX', bc.displacement[0] * 1e-3, 3)
writeData(f, 'DisplaceY', bc.displacement[1] * 1e-3, 3)
writeData(f, 'DisplaceZ', bc.displacement[2] * 1e-3, 3)
writeData(f, 'AngDisplaceX', bc.angular_displacement[0], 3)
writeData(f, 'AngDisplaceY', bc.angular_displacement[1], 3)
writeData(f, 'AngDisplaceZ', bc.angular_displacement[2], 3)
writeClos(f, 'FRegion', 2)
writeClos(f, 'Boundary_Conditions', 1)
writeOpen(f, 'Gravity', 1)
writeData(f, 'GravEnabled', int(self.__gravityEnable), 2)
writeData(f, 'GravAcc', self.__gravityValue, 2)
writeData(f, 'FloorEnabled', int(self.__floorEnable), 2)
writeClos(f, 'Gravity', 1)
writeOpen(f, 'Thermal', 1)
writeData(f, 'TempEnabled', int(self.__temperatureEnable), 2)
writeData(f, 'TempAmplitude', self.__temperatureVaryAmplitude, 2)
writeData(f, 'TempBase', self.__temperatureBaseValue, 2)
writeData(f, 'VaryTempEnabled', int(self.__temperatureVaryEnable), 2)
writeData(f, 'TempPeriod', self.__temperatureVaryPeriod, 2)
writeData(f, 'TempOffset', self.__temperatureVaryOffset, 2)
writeClos(f, 'Thermal', 1)
writeData(f, 'GrowthAmplitude', self.__growthAmplitude, 1)
writeClos(f, 'Environment', 0)
def writeSensors(self, f: TextIO):
"""
Write voxel sensors to a text file using the .vxa format.
Args:
f: File to write to
Returns:
None
"""
writeOpen(f, 'Sensors', 0)
for sensor in self.__sensors:
writeOpen(f, 'Sensor', 1)
if sensor.name is not None:
writeData(f, 'Name', sensor.name, 2)
writeData(f, 'Location', str(sensor.coords).replace('(', '').replace(',', '').replace(')', ''), 2)
writeData(f, 'Axis', sensor.axis.value, 2)
writeClos(f, 'Sensor', 1)
writeClos(f, 'Sensors', 0)
def writeTempControls(self, f: TextIO):
"""
Write temperature control element to a text file using the .vxa format.
Args:
f: File to write to
Returns:
None
"""
writeData(f, 'EnableHydrogelModel', int(self.__hydrogelModelEnable), 0)
writeOpen(f, 'TempControls', 0)
for group in self.__localTempControls:
writeOpen(f, 'Element', 1)
if group.name is not None:
writeData(f, 'Name', group.name, 2)
writeOpen(f, 'Locations', 2)
for loc in group.locations:
writeData(f, 'Location', str(loc).replace('(', '').replace(',', '').replace(')', ''), 3)
writeClos(f, 'Locations', 2)
writeOpen(f, 'Keyframes', 2)
for keyframe in group.keyframes:
writeOpen(f, 'Keyframe', 3)
writeData(f, 'TimeValue', keyframe.time_value, 4)
writeData(f, 'AmplitudePos', keyframe.amplitude_pos, 4)
writeData(f, 'AmplitudeNeg', keyframe.amplitude_neg, 4)
writeData(f, 'PercentPos', keyframe.percent_pos, 4)
writeData(f, 'Period', keyframe.period, 4)
writeData(f, 'PhaseOffset', keyframe.phase_offset, 4)
writeData(f, 'TempOffset', keyframe.temp_offset, 4)
writeData(f, 'ConstantTemp', int(keyframe.const_temp), 4)
writeData(f, 'SquareWave', int(keyframe.square_wave), 4)
writeClos(f, 'Keyframe', 3)
writeClos(f, 'Keyframes', 2)
writeClos(f, 'Element', 1)
writeClos(f, 'TempControls', 0)
def writeDisconnections(self, f: TextIO):
"""
Write bond disconnections to a text file using the .vxa format.
Args:
f: File to write to
Returns:
None
"""
writeOpen(f, 'Disconnections', 0)
for dc in self.__disconnections:
writeOpen(f, 'Break', 1)
writeData(f, 'Voxel1', str(dc.vx1).replace('(', '').replace(',', '').replace(')', ''), 2)
writeData(f, 'Voxel2', str(dc.vx2).replace('(', '').replace(',', '').replace(')', ''), 2)
writeClos(f, 'Break', 1)
writeClos(f, 'Disconnections', 0)
def runSim(self, filename: str = 'temp', value_map: int = 0, delete_files: bool = True, log_interval: int = -1, history_interval: int = -1, voxelyze_on_path: bool = False, wsl: bool = False):
"""
Run a Simulation object using Voxelyze.
This function will create a .vxa file, run the file with Voxelyze, and then load the .xml results file into
the results attribute of the Simulation object. Enabling delete_files will delete both the .vxa and .xml files
once the results have been loaded.
History files can be viewed using https://github.com/voxcraft/voxcraft-viz
Args:
filename: File name for .vxa and .xml files
value_map: Index of the desired value map type
log_interval: Set the step interval at which sensor log entries should be recorded, -1 to disable log
history_interval: Set the step interval at which history file entries should be recorded, -1 for default interval
delete_files: Enable/disable deleting simulation file when process is complete
voxelyze_on_path: Enable/disable using system Voxelyze rather than bundled Voxelyze
wsl: Enable/disable using Windows Subsystem for Linux with bundled Voxelyze
Returns:
None
"""
# Generate results file/directory names
filename = filename + '_' + str(self.id)
dirname = 'sim_results_' + str(self.date)
# Create results directory
if not os.path.exists(dirname):
os.makedirs(dirname)
# Create simulation file
self.saveVXA(dirname + '/' + filename)
if voxelyze_on_path:
command_string = 'voxelyze'
else:
# Check OS type
if os.name.startswith('nt'): # Windows
if wsl:
command_string = 'wsl "' + os.path.dirname(os.path.realpath(__file__)).replace('C:', '/mnt/c').replace('\\', '/') + '/utils/voxelyze"'
else:
command_string = f'"{os.path.dirname(os.path.realpath(__file__))}\\utils\\voxelyze.exe"'
else: # Linux
command_string = f'"{os.path.dirname(os.path.realpath(__file__))}/utils/voxelyze"'
command_string = command_string + ' -f ' + dirname + '/' + filename + '.vxa -o ' + dirname + '/' + filename + ' -vm ' + str(value_map) + ' -p'
if log_interval > 0:
command_string = command_string + ' -log-interval ' + str(log_interval)
if history_interval > 0:
command_string = command_string + ' -history-interval ' + str(history_interval)
print('Launching Voxelyze using: ' + command_string)
p = subprocess.Popen(command_string, shell=True)
p.wait()
# Open simulation results
f = open(dirname + '/' + filename + '.xml', 'r')
#print('Opening file: ' + f.name)
data = f.readlines()
f.close()
# Clear any previous results
self.results = []
# Find start and end locations for individual sensors
startLoc = []
endLoc = []
for row in range(len(data)): # tqdm(range(len(data)), desc='Finding sensor tags'):
data[row] = data[row].replace('\t', '')
if data[row][:-1] == '<Sensor>':
startLoc.append(row)
elif data[row][:-1] == '</Sensor>':
endLoc.append(row)
# Read the data from each sensor
sensor_count = len(startLoc)
if sensor_count > 0:
for sensor in range(sensor_count): # tqdm(range(len(startLoc)), desc='Reading sensor results'):
# Create a dictionary to hold the current sensor results
sensorResults = {}
# Read the data from each sensor tag
for row in range(startLoc[sensor]+1, endLoc[sensor]):
# Determine the current tag
tag = ''
for col in range(1, len(data[row])):
if data[row][col] == '>':
tag = data[row][1:col]
break
# Remove the tags and newline to determine the current value
data[row] = data[row].replace('<'+tag+'>', '').replace('</'+tag+'>', '')
value = data[row][:-1]
# Combine the current tag and value
if tag == 'Location':
coords = tuple(map(int, value.split(' ')))
x = coords[0] + self.__model.coords[0]
y = coords[1] + self.__model.coords[1]
z = coords[2] + self.__model.coords[2]
currentResult = {tag:(x, y, z)}
elif ' ' in value:
currentResult = {tag:tuple(map(float, value.split(' ')))}
else:
currentResult = {tag:float(value)}
# Add the current tag and value to the sensor results dictionary
sensorResults.update(currentResult)
# Append the results dictionary for the current sensor to the simulation results list
self.results.append(sensorResults)
else:
print('No sensors found')
if os.path.exists('value_map.txt'):
# Open simulation value map results
f = open('value_map.txt', 'r')
print('Opening file: ' + f.name)
data = f.readlines()
f.close()
# Clear any previous results
self.valueMap = np.zeros_like(self.__model.voxels, dtype=np.float32)
# Get map size
x_len = self.valueMap.shape[0]
y_len = self.valueMap.shape[1]
z_len = self.valueMap.shape[2]
for z in range(z_len): # tqdm(range(z_len), desc='Loading layers'):
vals = np.array(data[z][:-2].split(","), dtype=np.float32)
for y in range(y_len):
self.valueMap[:, y, z] = vals[y*x_len:(y+1)*x_len]
# Remove temporary files
if delete_files:
os.remove(dirname + '/' + filename + '.vxa')
os.remove(dirname + '/' + filename + '.xml')
if os.path.exists(dirname + '/value_map.txt'):
os.remove(dirname + '/value_map.txt')
def runSimVoxCad(self, filename: str = 'temp', delete_files: bool = True, voxcad_on_path: bool = False, wsl: bool = False):
"""
Run a Simulation object using the VoxCad GUI.
----
Example:
``simulation = Simulation(modelResult)``
``simulation.setCollision()``
``simulation.setStopCondition(StopCondition.TIME_VALUE, 0.01)``
``simulation.runSimVoxCad('collision_sim_1', delete_files=False)``
----
Args:
filename: File name
delete_files: Enable/disable deleting simulation file when VoxCad is closed
voxcad_on_path: Enable/disable using system VoxCad rather than bundled VoxCad
wsl: Enable/disable using Windows Subsystem for Linux with bundled VoxCad
Returns:
None
"""
# Generate file name
filename = filename + '_' + str(self.id)
# Create simulation file
self.saveVXA(filename)
if voxcad_on_path:
command_string = 'voxcad '
else:
# Check OS type
if os.name.startswith('nt'): # Windows
if wsl:
command_string = 'wsl "' + os.path.dirname(os.path.realpath(__file__)).replace('C:', '/mnt/c').replace('\\', '/') + '/utils/VoxCad" '
else:
command_string = f'"{os.path.dirname(os.path.realpath(__file__))}\\utils\\VoxCad.exe" '
else: # Linux
command_string = f'"{os.path.dirname(os.path.realpath(__file__))}/utils/VoxCad" '
command_string = command_string + filename + '.vxa'
print('Launching VoxCad using: ' + command_string)
p = subprocess.Popen(command_string, shell=True)
p.wait()
if delete_files:
print('Removing file: ' + filename + '.vxa')
os.remove(filename + '.vxa')
def saveResults(self, filename):
"""
Saves a simulation's results dictionary to a .csv file.
Args:
filename: Name of output file
Returns:
None
"""
# Get result table keys and size
keys = list(self.results[0].keys())
rows = len(self.results)
cols = len(keys)
# Create results file
f = open(filename + '.csv', 'w+')
print('Saving file: ' + f.name)
# Write headings
f.write('Sensor')
for c in range(cols):
f.write(',' + str(keys[c]))
f.write('\n')
# Write values
for r in range(rows):
f.write(str(r))
vals = list(self.results[r].values())
for c in range(cols):
f.write(',' + str(vals[c]).replace(',', ' '))
f.write('\n')
# Close file
f.close()
class MultiSimulation:
"""
MultiSimulation object that holds settings for generating a simulation and running multiple parallel trials of it using different parameters.
"""
def __init__(self, setup_fcn, setup_params: List[Tuple], thread_count: int = -1):
"""
Initialize a MultiSimulation object.
Args:
setup_fcn: Function to use for initializing Simulation objects. Should take a single tuple as an input and return a single Simulation object.
setup_params: List containing the desired input tuples for setup_fcn
thread_count: Maximum number of CPU threads, -1 to auto-detect
"""
self.__setup_fcn = setup_fcn
self.__setup_params = setup_params
if thread_count > 0:
self.__thread_count = thread_count
else:
max_threads = os.cpu_count()
self.__thread_count = max(1, max_threads - 2) # Default to leaving 1 core (2 threads) free, minimum of 1 thread
# Initialize result arrays
self.total_time = 0
self.displacement_result = multiprocessing.Array('d', len(setup_params))
self.time_result = multiprocessing.Array('d', len(setup_params))
def getParams(self):
"""
Get the current simulation setup parameters.
Returns:
List containing the current input tuples for setup_fcn
"""
return self.__setup_params
def setParams(self, setup_params: List[Tuple]):
"""
Update the simulation setup parameters.
Args:
setup_params: List containing the desired input tuples for setup_fcn
Returns:
None
"""
self.__setup_params = setup_params
self.displacement_result = multiprocessing.Array('d', len(setup_params))
self.time_result = multiprocessing.Array('d', len(setup_params))
def confirmSimCount(self):
"""
Print the number of simulations to be run and confirm that the user would like to continue.
Returns:
None
"""
# Check if results directory already exists
dirname = 'sim_results_' + str(date.today())
if os.path.exists(dirname):
print("WARNING: Previous results exist and may be overwritten: " + str(dirname))
print("Trials to run: " + str(len(self.__setup_params)))
print("Max CPU threads: " + str(self.__thread_count))
input("Press Enter to continue...")
def run(self, enable_log : bool = False, fine_log: bool = False):
"""
Run all simulation configurations and save the results.
History files can be viewed using https://github.com/voxcraft/voxcraft-viz
Args:
enable_log: Enable saving sensor log files
fine_log: If enabled, save entries in sensor logs and history files 100x as frequently
Returns:
None
"""
# Save start time
time_started = time.time()
# Set up simulations
sim_id = 0
sim_array = []
for config in tqdm(self.__setup_params, desc='Initializing simulations'):
sim = self.__setup_fcn(config)
# Use automatic sim id if one is not already set
if sim.id == 0:
sim.id = sim_id
sim_id = sim_id + 1
sim_array.append(sim)
# Initialize processing pool
p = multiprocessing.Pool(self.__thread_count, initializer=poolInit, initargs=(self.displacement_result, self.time_result))
if enable_log:
if fine_log:
p.map(simProcessLogFine, sim_array)
else:
p.map(simProcessLog, sim_array)
else:
p.map(simProcess, sim_array)
# Get elapsed time
time_finished = time.time()
self.total_time = time_finished - time_started
def export(self, filename: str, labels: List[str], parameters_only: bool = False):
"""
Export a CSV file containing simulation setup parameters and the corresponding simulation results.
Args:
filename: File name
labels: Column headers for simulation setup parameters
parameters_only: Only export setup parameters and not simulation results
Returns:
None
"""
# Add labels for results
if not parameters_only:
labels.append('Displacement (m)')
labels.append('Simulation Time (s)')
# Get result table size
rows = len(self.__setup_params)
cols = len(labels)
# Create results file
f = open(filename + '.csv', 'w+')
print('Saving file: ' + f.name)
# Write sim info
if not parameters_only:
f.write('Simulation Elapsed Time (mins),' + str(self.total_time / 60.0) + '\n')
f.write('Trial Count,' + str(rows) + '\n')
f.write('Max Thread Count,' + str(self.__thread_count) + '\n')
f.write('\n')
# Write headings
for c in range(cols):
f.write(labels[c] + ',')
f.write('\n')
# Write values
for r in range(rows):
for c in range(cols - 2):
f.write(str(self.__setup_params[r][c]) + ',')
if not parameters_only:
f.write(str(self.displacement_result[r]) + ',')
f.write(str(self.time_result[r]) + ',')
f.write('\n')
# Close file
f.close()
def exportVXA(self, filename: str, config_number: int = -1):
"""
Export VXA files for all or specified simulation configurations.
Args:
filename: File name
config_number: Config to export, -1 to export all configs
Returns:
None
"""
if config_number == -1:
for config in tqdm(self.__setup_params, desc='Saving simulations'):
sim = self.__setup_fcn(config)
sim.saveVXA(filename + '_' + str(config[0]))
else:
config = self.__setup_params[config_number]
sim = self.__setup_fcn(config)
sim.saveVXA(filename)
# Helper functions
def poolInit(disp_result_array, t_result_array):
"""
Initialize shared result variables.
Args:
disp_result_array: Multiprocessing array to hold displacement results
t_result_array: Multiprocessing array to hold time results
Returns:
None
"""
global disp_result, t_result
disp_result = disp_result_array
t_result = t_result_array
def simProcess(simulation: Simulation):
"""
Simulation process.
Args:
simulation: Simulation object to run
Returns:
None
"""
print('Process ' + str(simulation.id) + ' starting')
# Run simulation
time_process_started = time.time()
simulation.runSim('multisim', log_interval=-1, history_interval=100000, wsl=True)
time_process_finished = time.time()
# Read results
try:
disp_x = float(simulation.results[0]['Position'][0]) - float(simulation.results[0]['InitialPosition'][0])
disp_y = float(simulation.results[0]['Position'][1]) - float(simulation.results[0]['InitialPosition'][1])
disp_z = float(simulation.results[0]['Position'][2]) - float(simulation.results[0]['InitialPosition'][2])
disp_result[simulation.id] = np.sqrt((disp_x**2) + (disp_y**2) + (disp_z**2))
t_result[simulation.id] = time_process_finished - time_process_started
except IndexError:
print('Unable to load sensor results')
# Finished
print('Process ' + str(simulation.id) + ' finished')
def simProcessLog(simulation: Simulation):
"""
Simulation process.
Args:
simulation: Simulation object to run
Returns:
None
"""
print('Process ' + str(simulation.id) + ' starting')
# Run simulation
time_process_started = time.time()
simulation.runSim('multisim', log_interval=100000, history_interval=100000, wsl=True)
time_process_finished = time.time()
# Read results
try:
disp_x = float(simulation.results[0]['Position'][0]) - float(simulation.results[0]['InitialPosition'][0])
disp_y = float(simulation.results[0]['Position'][1]) - float(simulation.results[0]['InitialPosition'][1])
disp_z = float(simulation.results[0]['Position'][2]) - float(simulation.results[0]['InitialPosition'][2])
disp_result[simulation.id] = np.sqrt((disp_x**2) + (disp_y**2) + (disp_z**2))
t_result[simulation.id] = time_process_finished - time_process_started
except IndexError:
print('Unable to load sensor results')
# Finished
print('Process ' + str(simulation.id) + ' finished')
def simProcessLogFine(simulation: Simulation):
"""
Simulation process.
Args:
simulation: Simulation object to run
Returns:
None
"""
print('Process ' + str(simulation.id) + ' starting')
# Run simulation
time_process_started = time.time()
simulation.runSim('multisim', log_interval=1000, history_interval=1000, wsl=True)
time_process_finished = time.time()
# Read results
try:
disp_x = float(simulation.results[0]['Position'][0]) - float(simulation.results[0]['InitialPosition'][0])
disp_y = float(simulation.results[0]['Position'][1]) - float(simulation.results[0]['InitialPosition'][1])
disp_z = float(simulation.results[0]['Position'][2]) - float(simulation.results[0]['InitialPosition'][2])
disp_result[simulation.id] = np.sqrt((disp_x**2) + (disp_y**2) + (disp_z**2))
t_result[simulation.id] = time_process_finished - time_process_started
except IndexError:
print('Unable to load sensor results')
# Finished
print('Process ' + str(simulation.id) + ' finished')
Functions
def poolInit(disp_result_array, t_result_array)
-
Initialize shared result variables.
Args
disp_result_array
- Multiprocessing array to hold displacement results
t_result_array
- Multiprocessing array to hold time results
Returns
None
Expand source code
def poolInit(disp_result_array, t_result_array): """ Initialize shared result variables. Args: disp_result_array: Multiprocessing array to hold displacement results t_result_array: Multiprocessing array to hold time results Returns: None """ global disp_result, t_result disp_result = disp_result_array t_result = t_result_array
def simProcess(simulation: Simulation)
-
Simulation process.
Args
simulation
- Simulation object to run
Returns
None
Expand source code
def simProcess(simulation: Simulation): """ Simulation process. Args: simulation: Simulation object to run Returns: None """ print('Process ' + str(simulation.id) + ' starting') # Run simulation time_process_started = time.time() simulation.runSim('multisim', log_interval=-1, history_interval=100000, wsl=True) time_process_finished = time.time() # Read results try: disp_x = float(simulation.results[0]['Position'][0]) - float(simulation.results[0]['InitialPosition'][0]) disp_y = float(simulation.results[0]['Position'][1]) - float(simulation.results[0]['InitialPosition'][1]) disp_z = float(simulation.results[0]['Position'][2]) - float(simulation.results[0]['InitialPosition'][2]) disp_result[simulation.id] = np.sqrt((disp_x**2) + (disp_y**2) + (disp_z**2)) t_result[simulation.id] = time_process_finished - time_process_started except IndexError: print('Unable to load sensor results') # Finished print('Process ' + str(simulation.id) + ' finished')
def simProcessLog(simulation: Simulation)
-
Simulation process.
Args
simulation
- Simulation object to run
Returns
None
Expand source code
def simProcessLog(simulation: Simulation): """ Simulation process. Args: simulation: Simulation object to run Returns: None """ print('Process ' + str(simulation.id) + ' starting') # Run simulation time_process_started = time.time() simulation.runSim('multisim', log_interval=100000, history_interval=100000, wsl=True) time_process_finished = time.time() # Read results try: disp_x = float(simulation.results[0]['Position'][0]) - float(simulation.results[0]['InitialPosition'][0]) disp_y = float(simulation.results[0]['Position'][1]) - float(simulation.results[0]['InitialPosition'][1]) disp_z = float(simulation.results[0]['Position'][2]) - float(simulation.results[0]['InitialPosition'][2]) disp_result[simulation.id] = np.sqrt((disp_x**2) + (disp_y**2) + (disp_z**2)) t_result[simulation.id] = time_process_finished - time_process_started except IndexError: print('Unable to load sensor results') # Finished print('Process ' + str(simulation.id) + ' finished')
def simProcessLogFine(simulation: Simulation)
-
Simulation process.
Args
simulation
- Simulation object to run
Returns
None
Expand source code
def simProcessLogFine(simulation: Simulation): """ Simulation process. Args: simulation: Simulation object to run Returns: None """ print('Process ' + str(simulation.id) + ' starting') # Run simulation time_process_started = time.time() simulation.runSim('multisim', log_interval=1000, history_interval=1000, wsl=True) time_process_finished = time.time() # Read results try: disp_x = float(simulation.results[0]['Position'][0]) - float(simulation.results[0]['InitialPosition'][0]) disp_y = float(simulation.results[0]['Position'][1]) - float(simulation.results[0]['InitialPosition'][1]) disp_z = float(simulation.results[0]['Position'][2]) - float(simulation.results[0]['InitialPosition'][2]) disp_result[simulation.id] = np.sqrt((disp_x**2) + (disp_y**2) + (disp_z**2)) t_result[simulation.id] = time_process_finished - time_process_started except IndexError: print('Unable to load sensor results') # Finished print('Process ' + str(simulation.id) + ' finished')
Classes
class Axis (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Options for axes and planes.
Expand source code
class Axis(Enum): """ Options for axes and planes. """ NONE = -1 X = 0 Y = 1 Z = 2
Ancestors
- enum.Enum
Class variables
var NONE
var X
var Y
var Z
class BCShape (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Options for simulation boundary condition shapes.
Expand source code
class BCShape(Enum): """ Options for simulation boundary condition shapes. """ BOX = 0 CYLINDER = 1 SPHERE = 2
Ancestors
- enum.Enum
Class variables
var BOX
var CYLINDER
var SPHERE
class MultiSimulation (setup_fcn, setup_params: List[Tuple[]], thread_count: int = -1)
-
MultiSimulation object that holds settings for generating a simulation and running multiple parallel trials of it using different parameters.
Initialize a MultiSimulation object.
Args
setup_fcn
- Function to use for initializing Simulation objects. Should take a single tuple as an input and return a single Simulation object.
setup_params
- List containing the desired input tuples for setup_fcn
thread_count
- Maximum number of CPU threads, -1 to auto-detect
Expand source code
class MultiSimulation: """ MultiSimulation object that holds settings for generating a simulation and running multiple parallel trials of it using different parameters. """ def __init__(self, setup_fcn, setup_params: List[Tuple], thread_count: int = -1): """ Initialize a MultiSimulation object. Args: setup_fcn: Function to use for initializing Simulation objects. Should take a single tuple as an input and return a single Simulation object. setup_params: List containing the desired input tuples for setup_fcn thread_count: Maximum number of CPU threads, -1 to auto-detect """ self.__setup_fcn = setup_fcn self.__setup_params = setup_params if thread_count > 0: self.__thread_count = thread_count else: max_threads = os.cpu_count() self.__thread_count = max(1, max_threads - 2) # Default to leaving 1 core (2 threads) free, minimum of 1 thread # Initialize result arrays self.total_time = 0 self.displacement_result = multiprocessing.Array('d', len(setup_params)) self.time_result = multiprocessing.Array('d', len(setup_params)) def getParams(self): """ Get the current simulation setup parameters. Returns: List containing the current input tuples for setup_fcn """ return self.__setup_params def setParams(self, setup_params: List[Tuple]): """ Update the simulation setup parameters. Args: setup_params: List containing the desired input tuples for setup_fcn Returns: None """ self.__setup_params = setup_params self.displacement_result = multiprocessing.Array('d', len(setup_params)) self.time_result = multiprocessing.Array('d', len(setup_params)) def confirmSimCount(self): """ Print the number of simulations to be run and confirm that the user would like to continue. Returns: None """ # Check if results directory already exists dirname = 'sim_results_' + str(date.today()) if os.path.exists(dirname): print("WARNING: Previous results exist and may be overwritten: " + str(dirname)) print("Trials to run: " + str(len(self.__setup_params))) print("Max CPU threads: " + str(self.__thread_count)) input("Press Enter to continue...") def run(self, enable_log : bool = False, fine_log: bool = False): """ Run all simulation configurations and save the results. History files can be viewed using https://github.com/voxcraft/voxcraft-viz Args: enable_log: Enable saving sensor log files fine_log: If enabled, save entries in sensor logs and history files 100x as frequently Returns: None """ # Save start time time_started = time.time() # Set up simulations sim_id = 0 sim_array = [] for config in tqdm(self.__setup_params, desc='Initializing simulations'): sim = self.__setup_fcn(config) # Use automatic sim id if one is not already set if sim.id == 0: sim.id = sim_id sim_id = sim_id + 1 sim_array.append(sim) # Initialize processing pool p = multiprocessing.Pool(self.__thread_count, initializer=poolInit, initargs=(self.displacement_result, self.time_result)) if enable_log: if fine_log: p.map(simProcessLogFine, sim_array) else: p.map(simProcessLog, sim_array) else: p.map(simProcess, sim_array) # Get elapsed time time_finished = time.time() self.total_time = time_finished - time_started def export(self, filename: str, labels: List[str], parameters_only: bool = False): """ Export a CSV file containing simulation setup parameters and the corresponding simulation results. Args: filename: File name labels: Column headers for simulation setup parameters parameters_only: Only export setup parameters and not simulation results Returns: None """ # Add labels for results if not parameters_only: labels.append('Displacement (m)') labels.append('Simulation Time (s)') # Get result table size rows = len(self.__setup_params) cols = len(labels) # Create results file f = open(filename + '.csv', 'w+') print('Saving file: ' + f.name) # Write sim info if not parameters_only: f.write('Simulation Elapsed Time (mins),' + str(self.total_time / 60.0) + '\n') f.write('Trial Count,' + str(rows) + '\n') f.write('Max Thread Count,' + str(self.__thread_count) + '\n') f.write('\n') # Write headings for c in range(cols): f.write(labels[c] + ',') f.write('\n') # Write values for r in range(rows): for c in range(cols - 2): f.write(str(self.__setup_params[r][c]) + ',') if not parameters_only: f.write(str(self.displacement_result[r]) + ',') f.write(str(self.time_result[r]) + ',') f.write('\n') # Close file f.close() def exportVXA(self, filename: str, config_number: int = -1): """ Export VXA files for all or specified simulation configurations. Args: filename: File name config_number: Config to export, -1 to export all configs Returns: None """ if config_number == -1: for config in tqdm(self.__setup_params, desc='Saving simulations'): sim = self.__setup_fcn(config) sim.saveVXA(filename + '_' + str(config[0])) else: config = self.__setup_params[config_number] sim = self.__setup_fcn(config) sim.saveVXA(filename)
Methods
def confirmSimCount(self)
-
Print the number of simulations to be run and confirm that the user would like to continue.
Returns
None
Expand source code
def confirmSimCount(self): """ Print the number of simulations to be run and confirm that the user would like to continue. Returns: None """ # Check if results directory already exists dirname = 'sim_results_' + str(date.today()) if os.path.exists(dirname): print("WARNING: Previous results exist and may be overwritten: " + str(dirname)) print("Trials to run: " + str(len(self.__setup_params))) print("Max CPU threads: " + str(self.__thread_count)) input("Press Enter to continue...")
def export(self, filename: str, labels: List[str], parameters_only: bool = False)
-
Export a CSV file containing simulation setup parameters and the corresponding simulation results.
Args
filename
- File name
labels
- Column headers for simulation setup parameters
parameters_only
- Only export setup parameters and not simulation results
Returns
None
Expand source code
def export(self, filename: str, labels: List[str], parameters_only: bool = False): """ Export a CSV file containing simulation setup parameters and the corresponding simulation results. Args: filename: File name labels: Column headers for simulation setup parameters parameters_only: Only export setup parameters and not simulation results Returns: None """ # Add labels for results if not parameters_only: labels.append('Displacement (m)') labels.append('Simulation Time (s)') # Get result table size rows = len(self.__setup_params) cols = len(labels) # Create results file f = open(filename + '.csv', 'w+') print('Saving file: ' + f.name) # Write sim info if not parameters_only: f.write('Simulation Elapsed Time (mins),' + str(self.total_time / 60.0) + '\n') f.write('Trial Count,' + str(rows) + '\n') f.write('Max Thread Count,' + str(self.__thread_count) + '\n') f.write('\n') # Write headings for c in range(cols): f.write(labels[c] + ',') f.write('\n') # Write values for r in range(rows): for c in range(cols - 2): f.write(str(self.__setup_params[r][c]) + ',') if not parameters_only: f.write(str(self.displacement_result[r]) + ',') f.write(str(self.time_result[r]) + ',') f.write('\n') # Close file f.close()
def exportVXA(self, filename: str, config_number: int = -1)
-
Export VXA files for all or specified simulation configurations.
Args
filename
- File name
config_number
- Config to export, -1 to export all configs
Returns
None
Expand source code
def exportVXA(self, filename: str, config_number: int = -1): """ Export VXA files for all or specified simulation configurations. Args: filename: File name config_number: Config to export, -1 to export all configs Returns: None """ if config_number == -1: for config in tqdm(self.__setup_params, desc='Saving simulations'): sim = self.__setup_fcn(config) sim.saveVXA(filename + '_' + str(config[0])) else: config = self.__setup_params[config_number] sim = self.__setup_fcn(config) sim.saveVXA(filename)
def getParams(self)
-
Get the current simulation setup parameters.
Returns
List containing the current input tuples for setup_fcn
Expand source code
def getParams(self): """ Get the current simulation setup parameters. Returns: List containing the current input tuples for setup_fcn """ return self.__setup_params
def run(self, enable_log: bool = False, fine_log: bool = False)
-
Run all simulation configurations and save the results.
History files can be viewed using https://github.com/voxcraft/voxcraft-viz
Args
enable_log
- Enable saving sensor log files
fine_log
- If enabled, save entries in sensor logs and history files 100x as frequently
Returns
None
Expand source code
def run(self, enable_log : bool = False, fine_log: bool = False): """ Run all simulation configurations and save the results. History files can be viewed using https://github.com/voxcraft/voxcraft-viz Args: enable_log: Enable saving sensor log files fine_log: If enabled, save entries in sensor logs and history files 100x as frequently Returns: None """ # Save start time time_started = time.time() # Set up simulations sim_id = 0 sim_array = [] for config in tqdm(self.__setup_params, desc='Initializing simulations'): sim = self.__setup_fcn(config) # Use automatic sim id if one is not already set if sim.id == 0: sim.id = sim_id sim_id = sim_id + 1 sim_array.append(sim) # Initialize processing pool p = multiprocessing.Pool(self.__thread_count, initializer=poolInit, initargs=(self.displacement_result, self.time_result)) if enable_log: if fine_log: p.map(simProcessLogFine, sim_array) else: p.map(simProcessLog, sim_array) else: p.map(simProcess, sim_array) # Get elapsed time time_finished = time.time() self.total_time = time_finished - time_started
def setParams(self, setup_params: List[Tuple[]])
-
Update the simulation setup parameters.
Args
setup_params
- List containing the desired input tuples for setup_fcn
Returns
None
Expand source code
def setParams(self, setup_params: List[Tuple]): """ Update the simulation setup parameters. Args: setup_params: List containing the desired input tuples for setup_fcn Returns: None """ self.__setup_params = setup_params self.displacement_result = multiprocessing.Array('d', len(setup_params)) self.time_result = multiprocessing.Array('d', len(setup_params))
class Simulation (voxel_model, id_number: int = 0)
-
Simulation object that stores a VoxelModel and its associated simulation settings.
Initialize a Simulation object with default settings.
Models located at positive coordinate values will have their workspace size adjusted to maintain their position in the exported simulation. Models located at negative coordinate values will be shifted to the origin.
Args
voxel_model
- VoxelModel
Expand source code
class Simulation: """ Simulation object that stores a VoxelModel and its associated simulation settings. """ def __init__(self, voxel_model, id_number: int = 0): """ Initialize a Simulation object with default settings. Models located at positive coordinate values will have their workspace size adjusted to maintain their position in the exported simulation. Models located at negative coordinate values will be shifted to the origin. Args: voxel_model: VoxelModel """ # Simulation ID and start date self.id = id_number self.date = date.today() # Fit workspace and union with an empty object at the origin to clear offsets if object is raised self.__model = ((VoxelModel.copy(voxel_model).fitWorkspace()) | empty(num_materials=(voxel_model.materials.shape[1] - 1), resolution=voxel_model.resolution)).removeDuplicateMaterials() # Simulator ############## # Integration self.__integrator = 0 self.__dtFraction = 1.0 # Damping self.__dampingBond = 1.0 # (0-1) Bulk material damping self.__dampingEnvironment = 0.0001 # (0-0.1) Damping caused by fluid environment # Collisions self.__collisionEnable = False self.__collisionDamping = 1.0 # (0-2) Elastic vs inelastic conditions self.__collisionSystem = 3 self.__collisionHorizon = 3 # Features self.__blendingEnable = False self.__xMixRadius = 0 self.__yMixRadius = 0 self.__zMixRadius = 0 self.__blendingModel = 0 self.__polyExp = 1 self.__volumeEffectsEnable = False self.__hydrogelModelEnable = False # Stop conditions self.__stopConditionType = StopCondition.NONE self.__stopConditionValue = 0.0 # Equilibrium mode self.__equilibriumModeEnable = False # Environment ############ # Boundary conditions self.__bcRegions = [] # Gravity self.__gravityEnable = True self.__gravityValue = -9.81 self.__floorEnable = True # Thermal self.__temperatureEnable = False self.__temperatureBaseValue = 25.0 self.__temperatureVaryEnable = False self.__temperatureVaryAmplitude = 0.0 self.__temperatureVaryPeriod = 0.0 self.__temperatureVaryOffset = 0.0 self.__growthAmplitude = 0.0 # Sensors ####### self.__sensors = [] # Temperature Controls ####### self.__currentTempControlGroup = 0 self.__localTempControls = [] # Disconnected Bonds ####### self.__disconnections = [] # Results ################ self.results = [] self.valueMap = np.zeros_like(voxel_model.voxels, dtype=np.float32) @classmethod def copy(cls, simulation): """ Create new Simulation object with the same settings as an existing Simulation object. Args: simulation: Simulation to copy Returns: Simulation """ # Create new simulation object and copy attribute values new_simulation = cls(simulation.__model) new_simulation.__dict__ = simulation.__dict__.copy() # Update ID and date new_simulation.id = simulation.id + 1 new_simulation.date = date.today() # Make lists copies instead of references new_simulation.__bcRegions = simulation.__bcRegions.copy() new_simulation.__sensors = simulation.__sensors.copy() new_simulation.__localTempControls = simulation.__localTempControls.copy() new_simulation.__disconnections = simulation.__disconnections.copy() new_simulation.results = simulation.results.copy() new_simulation.valueMap = simulation.valueMap.copy() return new_simulation # Configure settings ################################## def setModel(self, voxel_model): """ Set the model for a simulation. Models located at positive coordinate values will have their workspace size adjusted to maintain their position in the exported simulation. Models located at negative coordinate values will be shifted to the origin. Args: voxel_model: VoxelModel Returns: None """ # Fit workspace and union with an empty object at the origin to clear offsets if object is raised self.__model = ((VoxelModel.copy(voxel_model).fitWorkspace()) | empty(num_materials=(voxel_model.materials.shape[1] - 1))).removeDuplicateMaterials() def setDamping(self, bond: float = 1.0, environment: float = 0.0001): """ Set simulation damping parameters. Environment damping can be used to simulate fluid environments. 0 represents a vacuum and larger values represent a viscous fluid. Args: bond: Voxel bond damping (0-1) environment: Environment damping (0-0.1) Returns: None """ self.__dampingBond = bond self.__dampingEnvironment = environment def setCollision(self, enable: bool = True, damping: float = 1.0): """ Set simulation collision parameters. A damping value of 0 represents completely elastic collisions and higher values represent inelastic collisions. Args: enable: Enable/disable collisions damping: Collision damping (0-2) Returns: None """ self.__collisionEnable = enable self.__collisionDamping = damping def setHydrogelModel(self, enable: bool = True): """ Set hydrogel model parameters. Args: enable: Enable/disable hydrogel model Returns: None """ self.__hydrogelModelEnable = enable def setStopCondition(self, condition: StopCondition = StopCondition.NONE, value: float = 0): """ Set simulation stop condition. Args: condition: Stop condition type, set using StopCondition class value: Stop condition value Returns: None """ self.__stopConditionType = condition self.__stopConditionValue = value def setEquilibriumMode(self, enable: bool = True): """ Set simulation equilibrium mode. Args: enable: Enable/disable equilibrium mode Returns: None """ self.__equilibriumModeEnable = enable def setGravity(self, enable: bool = True, value: float = -9.81, enable_floor: bool = True): """ Set simulation gravity parameters. Args: enable: Enable/disable gravity value: Acceleration due to gravity in m/sec^2 enable_floor: Enable/disable ground plane Returns: None """ self.__gravityEnable = enable self.__gravityValue = value self.__floorEnable = enable_floor def setFixedThermal(self, enable: bool = True, base_temp: float = 25.0, growth_amplitude: float = 0.0): """ Set a fixed environment temperature. Args: enable: Enable/disable temperature base_temp: Temperature in degrees C growth_amplitude: Set to 1 to enable expansion from base size Returns: None """ self.__temperatureEnable = enable self.__temperatureBaseValue = base_temp self.__temperatureVaryEnable = False self.__growthAmplitude = growth_amplitude def setVaryingThermal(self, enable: bool = True, base_temp: float = 25.0, amplitude: float = 0.0, period: float = 1.0, offset: float = 0.0, growth_amplitude: float = 1.0): """ Set a varying environment temperature. Args: enable: Enable/disable temperature base_temp: Base temperature in degrees C amplitude: Temperature fluctuation amplitude period: Temperature fluctuation period offset: Temperature offset (not currently supported) growth_amplitude: Set to 1 to enable expansion from base size Returns: None """ self.__temperatureEnable = enable self.__temperatureBaseValue = base_temp self.__temperatureVaryEnable = enable self.__temperatureVaryAmplitude = amplitude self.__temperatureVaryPeriod = period self.__temperatureVaryOffset = offset self.__growthAmplitude = growth_amplitude # Read settings ################################## def getModel(self): """ Get the simulation model. Returns: VoxelModel """ return self.__model def getVoxelDim(self): """ Get the side dimension of a voxel in mm. Returns: Float """ res = self.__model.resolution return (1.0/res) * 0.001 def getDamping(self): """ Get simulation damping parameters. Returns: Voxel bond damping, Environment damping """ return self.__dampingBond, self.__dampingEnvironment def getCollision(self): """ Get simulation collision parameters. Returns: Enable/disable collisions, Collision damping """ return self.__collisionEnable, self.__collisionDamping def getHydrogelModel(self): """ Get hydrogel model parameters. Returns: Enable/disable hydrogel model """ return self.__hydrogelModelEnable def getStopCondition(self): """ Get simulation stop condition. Returns: Stop condition type, Stop condition value """ return self.__stopConditionType, self.__stopConditionValue def getEquilibriumMode(self): """ Get simulation equilibrium mode. Returns: Enable/disable equilibrium mode """ return self.__equilibriumModeEnable def getGravity(self): """ Get simulation gravity parameters. Returns: Enable/disable gravity, Acceleration due to gravity in m/sec^2, Enable/disable ground plane """ return self.__gravityEnable, self.__gravityValue, self.__floorEnable def getThermal(self): """ Get simulation temperature parameters. Returns: Enable/disable temperature, Base temperature in degrees C, Enable/disable temperature fluctuation, Temperature fluctuation amplitude, Temperature fluctuation period, Temperature offset """ return self.__temperatureEnable, self.__temperatureBaseValue, self.__temperatureVaryEnable, self.__temperatureVaryAmplitude, self.__temperatureVaryPeriod, self.__temperatureVaryOffset # Add forces, constraints, and sensors ################################## # Boundary condition sizes and positions are expressed as percentages of the overall model size # radius is a percentage of the largest model dimension # Fixed DOF bits correspond to: Rz, Ry, Rx, Z, Y, X # 0: Free, force will be applied # 1: Fixed, displacement will be applied # Displacement is expressed in mm def clearBoundaryConditions(self): """ Remove all boundary conditions from a Simulation object. Returns: None """ self.__bcRegions = [] # self.__bcVoxels = [] # Default box boundary condition is a fixed constraint in the YZ plane def addBoundaryConditionVoxel(self, position: Tuple[int, int, int] = (0, 0, 0), fixed_dof: int = 0b111111, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None): """ Add a boundary condition at a specific voxel. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied. Args: position: Position in voxels fixed_dof: Fixed degrees of freedom force: Force vector in N displacement: Displacement vector in mm torque: Torque values in Nm angular_displacement: Angular displacement values in deg name: Boundary condition name Returns: None """ x = position[0] - self.__model.coords[0] y = position[1] - self.__model.coords[1] z = position[2] - self.__model.coords[2] x_len = int(self.__model.voxels.shape[0]) y_len = int(self.__model.voxels.shape[1]) z_len = int(self.__model.voxels.shape[2]) pos = ((x+0.5)/x_len, (y+0.5)/y_len, (z+0.5)/z_len) radius = 0.49/x_len bc = _BCRegion(name, BCShape.SPHERE, pos, (0.0, 0.0, 0.0), radius, fixed_dof, force, displacement, torque, angular_displacement) self.__bcRegions.append(bc) # Default box boundary condition is a fixed constraint in the XY plane (bottom layer) def addBoundaryConditionBox(self, position: Tuple[float, float, float] = (0.0, 0.0, 0.0), size: Tuple[float, float, float] = (1.0, 1.0, 0.01), fixed_dof: int = 0b111111, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None): """ Add a box-shaped boundary condition. Boundary condition position and size are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied. Args: position: Box corner position (0-1) size: Box size (0-1) fixed_dof: Fixed degrees of freedom force: Force vector in N displacement: Displacement vector in mm torque: Torque values in Nm angular_displacement: Angular displacement values in deg name: Boundary condition name Returns: None """ bc = _BCRegion(name, BCShape.BOX, position, size, 0, fixed_dof, force, displacement, torque, angular_displacement) self.__bcRegions.append(bc) # Default sphere boundary condition is a fixed constraint centered in the model def addBoundaryConditionSphere(self, position: Tuple[float, float, float] = (0.5, 0.5, 0.5), radius: float = 0.05, fixed_dof: int = 0b111111, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None): """ Add a spherical boundary condition. Boundary condition position and radius are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied. Args: position: Sphere center position (0-1) radius: Sphere radius (0-1) fixed_dof: Fixed degrees of freedom force: Force vector in N displacement: Displacement vector in mm torque: Torque values in Nm angular_displacement: Angular displacement values in deg name: Boundary condition name Returns: None """ bc = _BCRegion(name, BCShape.SPHERE, position, (0.0, 0.0, 0.0), radius, fixed_dof, force, displacement, torque, angular_displacement) self.__bcRegions.append(bc) # Default cylinder boundary condition is a fixed constraint centered in the model def addBoundaryConditionCylinder(self, position: Tuple[float, float, float] = (0.45, 0.5, 0.5), axis: Axis = Axis.X, height: float = 0.1, radius: float = 0.05, fixed_dof: int = 0b111111, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None): """ Add a cylindrical boundary condition. Boundary condition position and size are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied. Args: position: Boundary condition origin position (0-1) axis: Cylinder axis (0-2) height: Cylinder height (0-1) radius: Cylinder radius (0-1) fixed_dof: Fixed degrees of freedom force: Force vector in N displacement: Displacement vector in mm torque: Torque values in Nm angular_displacement: Angular displacement values in deg name: Boundary condition name Returns: None """ size = [0.0, 0.0, 0.0] size[axis.value] = height bc = _BCRegion(name, BCShape.CYLINDER, position, (size[0], size[1], size[2]), radius, fixed_dof, force, displacement, torque, angular_displacement) self.__bcRegions.append(bc) def clearSensors(self): """ Remove all sensors from a Simulation object. Returns: None """ self.__sensors = [] def addSensor(self, location: Tuple[int, int, int] = (0, 0, 0), axis: Axis = Axis.NONE, name: str = None): # TODO: Make Voxelyze use axis parameter """ Add a sensor to a voxel. Args: location: Sensor location in voxels axis: Sensor measurement axis name: Sensor name Returns: None """ x = location[0] - self.__model.coords[0] y = location[1] - self.__model.coords[1] z = location[2] - self.__model.coords[2] sensor = _Sensor(name, (x, y, z), axis) self.__sensors.append(sensor) if not self.__model.isOccupied((x, y, z)): print('WARNING: No material present at sensor voxel ' + str((x, y, z))) def clearDisconnections(self): """ Clear all disconnected voxel bonds. Returns: None """ self.__disconnections = [] def addDisconnection(self, voxel_1: Tuple[int, int, int], voxel_2: Tuple[int, int, int]): """ Specify a pair of voxels which should be disconnected Args: voxel_1: Coordinates in voxels voxel_2: Coordinates in voxels Returns: None """ dc = _Disconnection(voxel_1, voxel_2) self.__disconnections.append(dc) def addTempControlGroup(self, locations: List[Tuple[int, int, int]] = None, name: str = None): """ Add a new temperature control group and select it. Args: locations: Control element locations in voxels as a list of tuples name: Group name Returns: None """ # Enable temperature control self.__temperatureEnable = True self.__temperatureVaryEnable = True if locations is None: print('No locations provided - applying temperature control group to entire model') x_len = self.__model.voxels.shape[0] y_len = self.__model.voxels.shape[1] z_len = self.__model.voxels.shape[2] locations = [] for x in range(x_len): for y in range(y_len): for z in range(z_len): locations.append((x, y, z)) group = _TempControlGroup(name, locations, []) self.__localTempControls.append(group) self.__currentTempControlGroup = len(self.__localTempControls)-1 def removeTempControlGroup(self, index: int = 0): """ Remove a temperature control group by index. Args: index: Temperature control group index Returns: Name of removed group """ group = self.__localTempControls.pop(index) self.__currentTempControlGroup = len(self.__localTempControls)-1 return group.name def selectTempControlGroup(self, index: int = 0): """ Select which keyframe new temperature control elements should be added to. Args: index: Temperature control group index Returns: None """ self.__currentTempControlGroup = index def clearTempControlGroups(self): """ Clear all temperature control groups. Returns: None """ self.__currentTempControlGroup = 0 self.__localTempControls = [] def cleanTempControlGroups(self): """ Remove invalid temperature control groups. Invalid groups have no keyframes, or have no target voxels. Returns: Number of groups removed """ removed_groups = [] for g in range(len(self.__localTempControls)): g = g - len(removed_groups) group = self.__localTempControls[g] if len(group.keyframes) == 0 or len(group.locations) == 0: removed_groups.append(self.removeTempControlGroup(g)) self.__currentTempControlGroup = len(self.__localTempControls)-1 named_groups = [] for n in removed_groups: if n is not None: named_groups.append(n) if len(removed_groups) == 0: pass elif len(named_groups) == 0: print('Removed ' + str(len(removed_groups)) + ' temperature control groups') else: print('Removed ' + str(len(removed_groups)) + ' temperature control groups, including: ' + str(named_groups)) return len(removed_groups) def clearKeyframes(self): """ Remove all keyframes assigned to the current temperature control group. Returns: None """ g = self.__currentTempControlGroup self.__localTempControls[g].keyframes = [] def addKeyframe(self, time_value: float = 0, amplitude_pos: float = 0, amplitude_neg: float = -1, percent_pos: float = 0.5, period: float = 1.0, phase_offset: float = 0, temp_offset: float = 0, const_temp: bool = False, square_wave: bool = False): """ Add a keyframe to a temperature control group. Args: time_value: Time at which keyframe should take effect (sec) amplitude_pos: Control element positive temperature amplitude (deg C) amplitude_neg: Control element negative temperature amplitude (deg C) percent_pos: Percent of period spanned by positive temperature amplitude (0-1) period: Period of the control signal (sec) phase_offset: Control element phase offset for time-varying thermal (rad) temp_offset: Control element temperature offset for time-varying thermal (deg C) const_temp: Enable/disable setting a constant target temperature that respects heating/cooling rates square_wave: Enable/disable converting signal to a square wave (positive -> a = amplitude1, negative -> a = 0) Returns: None """ amplitude_pos = max(amplitude_pos, 0) # amplitude_pos must be positive if amplitude_neg < 0: # If amplitude neg is not given (or not negative) use amplitude_pos instead amplitude_neg = amplitude_pos g = self.__currentTempControlGroup kf = _Keyframe(time_value, amplitude_pos, amplitude_neg, percent_pos, period, phase_offset, temp_offset, const_temp, square_wave) self.__localTempControls[g].keyframes.append(kf) def initializeTempMap(self): """ Initialize temperature control groups to which keyframes will be stored when using applyTempMap. Returns: None """ # Clear any existing temperature controls self.clearTempControlGroups() # Get map size x_len = self.__model.voxels.shape[0] y_len = self.__model.voxels.shape[1] z_len = self.__model.voxels.shape[2] # Generate empty control element for each voxel for x in range(x_len): for y in range(y_len): for z in range(z_len): self.addTempControlGroup([(x, y, z)]) def applyTempMap(self, time_value, amp_pos_map, amp_neg_map=None, percent_pos_map=None, period_map=None, phase_map=None, offset_map=None, const_temp_map=None, square_wave_map=None): """ Set the simulation temperature control elements based on a value maps of target temperature settings. This function relies on the temperature control groups being in a specific order. initializeTempMap should be called prior to running this function. Args: time_value: Time at which keyframe should take effect (sec) amp_pos_map: Array of target positive temperature amplitudes for each voxel (deg C) amp_neg_map: Array of target negative temperature amplitudes for each voxel (deg C) percent_pos_map: Array containing percent of period spanned by positive temperature amplitude for each voxel period_map: Array of signal periods for each voxel (sec) phase_map: Array of phase offsets for each voxel (rad) offset_map: Array of temperature offsets for each voxel (deg C) const_temp_map: Array of boolean values to enable/disable constant temperature mode square_wave_map: Array of boolean values to enable/disable square wave mode Returns: None """ # Get map size x_len = amp_pos_map.shape[0] y_len = amp_pos_map.shape[1] z_len = amp_pos_map.shape[2] # Generate the control element for each voxel for x in range(x_len): for y in range(y_len): for z in range(z_len): if abs(amp_pos_map[x, y, z]) > FLOATING_ERROR or ((amp_neg_map is not None) and (abs(amp_neg_map[x, y, z]) > FLOATING_ERROR)): # If voxel is not empty amplitude_pos = amp_pos_map[x, y, z] if amp_neg_map is None: amplitude_neg = amp_pos_map[x, y, z] else: amplitude_neg = amp_neg_map[x, y, z] if percent_pos_map is None: percent_pos = 0.5 else: percent_pos = percent_pos_map[x, y, z] if period_map is None: period = 1.0 else: period = period_map[x, y, z] if phase_map is None: phase_offset = 0 else: phase_offset = phase_map[x, y, z] if offset_map is None: temp_offset = 0 else: temp_offset = offset_map[x, y, z] if const_temp_map is None: const_temp = False else: const_temp = const_temp_map[x, y, z] if square_wave_map is None: square_wave = False else: square_wave = square_wave_map[x, y, z] kf = _Keyframe(time_value, amplitude_pos, amplitude_neg, percent_pos, period, phase_offset, temp_offset, const_temp, square_wave) g = z + y*z_len + x*y_len*z_len self.__localTempControls[g].keyframes.append(kf) # Export simulation ################################## # Export simulation object to .vxa file for import into VoxCad or Voxelyze def saveVXA(self, filename: str, compression: bool = False): """ Save model data to a .vxa file The VoxCad simulation file format stores all the data contained in a .vxc file (geometry, material palette) plus the simulation setup (simulation parameters, environment settings, boundary conditions). This format supports compression for the voxel data. Enabling compression allows for larger models, but it may introduce geometry errors that particularly affect small models. The .vxa file type can be opened using VoxCad simulation software. However, it cannot currently be reopened by a VoxelFuse script. Args: filename: File name compression: Enable/disable voxel data compression Returns: None """ self.cleanTempControlGroups() f = open(filename + '.vxa', 'w+') print('Saving file: ' + f.name) writeHeader(f, '1.0', 'ISO-8859-1') writeOpen(f, 'VXA Version="' + str(1.1) + '"', 0) self.writeSimData(f) self.writeEnvironmentData(f) self.writeSensors(f) self.writeTempControls(f) self.writeDisconnections(f) self.__model.writeVXCData(f, compression) writeClos(f, 'VXA', 0) f.close() # Write simulator settings to file def writeSimData(self, f: TextIO): """ Write simulation parameters to a text file using the .vxa format. Args: f: File to write to Returns: None """ # Simulator settings writeOpen(f, 'Simulator', 0) writeOpen(f, 'Integration', 1) writeData(f, 'Integrator', self.__integrator, 2) writeData(f, 'DtFrac', self.__dtFraction, 2) writeClos(f, 'Integration', 1) writeOpen(f, 'Damping', 1) writeData(f, 'BondDampingZ', self.__dampingBond, 2) writeData(f, 'ColDampingZ', self.__collisionDamping, 2) writeData(f, 'SlowDampingZ', self.__dampingEnvironment, 2) writeClos(f, 'Damping', 1) writeOpen(f, 'Collisions', 1) writeData(f, 'SelfColEnabled', int(self.__collisionEnable), 2) writeData(f, 'ColSystem', self.__collisionSystem, 2) writeData(f, 'CollisionHorizon', self.__collisionHorizon, 2) writeClos(f, 'Collisions', 1) writeOpen(f, 'Features', 1) writeData(f, 'BlendingEnabled', int(self.__blendingEnable), 2) writeData(f, 'XMixRadius', self.__xMixRadius, 2) writeData(f, 'YMixRadius', self.__yMixRadius, 2) writeData(f, 'ZMixRadius', self.__zMixRadius, 2) writeData(f, 'BlendModel', self.__blendingModel, 2) writeData(f, 'PolyExp', self.__polyExp, 2) writeData(f, 'VolumeEffectsEnabled', int(self.__volumeEffectsEnable), 2) writeClos(f, 'Features', 1) writeOpen(f, 'StopCondition', 1) writeData(f, 'StopConditionType', self.__stopConditionType.value, 2) writeData(f, 'StopConditionValue', self.__stopConditionValue, 2) writeClos(f, 'StopCondition', 1) writeOpen(f, 'EquilibriumMode', 1) writeData(f, 'EquilibriumModeEnabled', int(self.__equilibriumModeEnable), 2) writeClos(f, 'EquilibriumMode', 1) writeClos(f, 'Simulator', 0) # Write environment settings to file def writeEnvironmentData(self, f: TextIO): """ Write simulation environment parameters to a text file using the .vxa format. Args: f: File to write to Returns: None """ # Environment settings writeOpen(f, 'Environment', 0) writeOpen(f, 'Boundary_Conditions', 1) writeData(f, 'NumBCs', len(self.__bcRegions), 2) for bc in self.__bcRegions: writeOpen(f, 'FRegion', 2) if bc.name is not None: writeData(f, 'Name', bc.name, 3) writeData(f, 'PrimType', int(bc.shape.value), 3) writeData(f, 'X', bc.position[0], 3) writeData(f, 'Y', bc.position[1], 3) writeData(f, 'Z', bc.position[2], 3) writeData(f, 'dX', bc.size[0], 3) writeData(f, 'dY', bc.size[1], 3) writeData(f, 'dZ', bc.size[2], 3) writeData(f, 'Radius', bc.radius, 3) writeData(f, 'R', bc.color[0], 3) writeData(f, 'G', bc.color[1], 3) writeData(f, 'B', bc.color[2], 3) writeData(f, 'alpha', bc.color[3], 3) writeData(f, 'DofFixed', bc.fixed_dof, 3) writeData(f, 'ForceX', bc.force[0], 3) writeData(f, 'ForceY', bc.force[1], 3) writeData(f, 'ForceZ', bc.force[2], 3) writeData(f, 'TorqueX', bc.torque[0], 3) writeData(f, 'TorqueY', bc.torque[1], 3) writeData(f, 'TorqueZ', bc.torque[2], 3) writeData(f, 'DisplaceX', bc.displacement[0] * 1e-3, 3) writeData(f, 'DisplaceY', bc.displacement[1] * 1e-3, 3) writeData(f, 'DisplaceZ', bc.displacement[2] * 1e-3, 3) writeData(f, 'AngDisplaceX', bc.angular_displacement[0], 3) writeData(f, 'AngDisplaceY', bc.angular_displacement[1], 3) writeData(f, 'AngDisplaceZ', bc.angular_displacement[2], 3) writeClos(f, 'FRegion', 2) writeClos(f, 'Boundary_Conditions', 1) writeOpen(f, 'Gravity', 1) writeData(f, 'GravEnabled', int(self.__gravityEnable), 2) writeData(f, 'GravAcc', self.__gravityValue, 2) writeData(f, 'FloorEnabled', int(self.__floorEnable), 2) writeClos(f, 'Gravity', 1) writeOpen(f, 'Thermal', 1) writeData(f, 'TempEnabled', int(self.__temperatureEnable), 2) writeData(f, 'TempAmplitude', self.__temperatureVaryAmplitude, 2) writeData(f, 'TempBase', self.__temperatureBaseValue, 2) writeData(f, 'VaryTempEnabled', int(self.__temperatureVaryEnable), 2) writeData(f, 'TempPeriod', self.__temperatureVaryPeriod, 2) writeData(f, 'TempOffset', self.__temperatureVaryOffset, 2) writeClos(f, 'Thermal', 1) writeData(f, 'GrowthAmplitude', self.__growthAmplitude, 1) writeClos(f, 'Environment', 0) def writeSensors(self, f: TextIO): """ Write voxel sensors to a text file using the .vxa format. Args: f: File to write to Returns: None """ writeOpen(f, 'Sensors', 0) for sensor in self.__sensors: writeOpen(f, 'Sensor', 1) if sensor.name is not None: writeData(f, 'Name', sensor.name, 2) writeData(f, 'Location', str(sensor.coords).replace('(', '').replace(',', '').replace(')', ''), 2) writeData(f, 'Axis', sensor.axis.value, 2) writeClos(f, 'Sensor', 1) writeClos(f, 'Sensors', 0) def writeTempControls(self, f: TextIO): """ Write temperature control element to a text file using the .vxa format. Args: f: File to write to Returns: None """ writeData(f, 'EnableHydrogelModel', int(self.__hydrogelModelEnable), 0) writeOpen(f, 'TempControls', 0) for group in self.__localTempControls: writeOpen(f, 'Element', 1) if group.name is not None: writeData(f, 'Name', group.name, 2) writeOpen(f, 'Locations', 2) for loc in group.locations: writeData(f, 'Location', str(loc).replace('(', '').replace(',', '').replace(')', ''), 3) writeClos(f, 'Locations', 2) writeOpen(f, 'Keyframes', 2) for keyframe in group.keyframes: writeOpen(f, 'Keyframe', 3) writeData(f, 'TimeValue', keyframe.time_value, 4) writeData(f, 'AmplitudePos', keyframe.amplitude_pos, 4) writeData(f, 'AmplitudeNeg', keyframe.amplitude_neg, 4) writeData(f, 'PercentPos', keyframe.percent_pos, 4) writeData(f, 'Period', keyframe.period, 4) writeData(f, 'PhaseOffset', keyframe.phase_offset, 4) writeData(f, 'TempOffset', keyframe.temp_offset, 4) writeData(f, 'ConstantTemp', int(keyframe.const_temp), 4) writeData(f, 'SquareWave', int(keyframe.square_wave), 4) writeClos(f, 'Keyframe', 3) writeClos(f, 'Keyframes', 2) writeClos(f, 'Element', 1) writeClos(f, 'TempControls', 0) def writeDisconnections(self, f: TextIO): """ Write bond disconnections to a text file using the .vxa format. Args: f: File to write to Returns: None """ writeOpen(f, 'Disconnections', 0) for dc in self.__disconnections: writeOpen(f, 'Break', 1) writeData(f, 'Voxel1', str(dc.vx1).replace('(', '').replace(',', '').replace(')', ''), 2) writeData(f, 'Voxel2', str(dc.vx2).replace('(', '').replace(',', '').replace(')', ''), 2) writeClos(f, 'Break', 1) writeClos(f, 'Disconnections', 0) def runSim(self, filename: str = 'temp', value_map: int = 0, delete_files: bool = True, log_interval: int = -1, history_interval: int = -1, voxelyze_on_path: bool = False, wsl: bool = False): """ Run a Simulation object using Voxelyze. This function will create a .vxa file, run the file with Voxelyze, and then load the .xml results file into the results attribute of the Simulation object. Enabling delete_files will delete both the .vxa and .xml files once the results have been loaded. History files can be viewed using https://github.com/voxcraft/voxcraft-viz Args: filename: File name for .vxa and .xml files value_map: Index of the desired value map type log_interval: Set the step interval at which sensor log entries should be recorded, -1 to disable log history_interval: Set the step interval at which history file entries should be recorded, -1 for default interval delete_files: Enable/disable deleting simulation file when process is complete voxelyze_on_path: Enable/disable using system Voxelyze rather than bundled Voxelyze wsl: Enable/disable using Windows Subsystem for Linux with bundled Voxelyze Returns: None """ # Generate results file/directory names filename = filename + '_' + str(self.id) dirname = 'sim_results_' + str(self.date) # Create results directory if not os.path.exists(dirname): os.makedirs(dirname) # Create simulation file self.saveVXA(dirname + '/' + filename) if voxelyze_on_path: command_string = 'voxelyze' else: # Check OS type if os.name.startswith('nt'): # Windows if wsl: command_string = 'wsl "' + os.path.dirname(os.path.realpath(__file__)).replace('C:', '/mnt/c').replace('\\', '/') + '/utils/voxelyze"' else: command_string = f'"{os.path.dirname(os.path.realpath(__file__))}\\utils\\voxelyze.exe"' else: # Linux command_string = f'"{os.path.dirname(os.path.realpath(__file__))}/utils/voxelyze"' command_string = command_string + ' -f ' + dirname + '/' + filename + '.vxa -o ' + dirname + '/' + filename + ' -vm ' + str(value_map) + ' -p' if log_interval > 0: command_string = command_string + ' -log-interval ' + str(log_interval) if history_interval > 0: command_string = command_string + ' -history-interval ' + str(history_interval) print('Launching Voxelyze using: ' + command_string) p = subprocess.Popen(command_string, shell=True) p.wait() # Open simulation results f = open(dirname + '/' + filename + '.xml', 'r') #print('Opening file: ' + f.name) data = f.readlines() f.close() # Clear any previous results self.results = [] # Find start and end locations for individual sensors startLoc = [] endLoc = [] for row in range(len(data)): # tqdm(range(len(data)), desc='Finding sensor tags'): data[row] = data[row].replace('\t', '') if data[row][:-1] == '<Sensor>': startLoc.append(row) elif data[row][:-1] == '</Sensor>': endLoc.append(row) # Read the data from each sensor sensor_count = len(startLoc) if sensor_count > 0: for sensor in range(sensor_count): # tqdm(range(len(startLoc)), desc='Reading sensor results'): # Create a dictionary to hold the current sensor results sensorResults = {} # Read the data from each sensor tag for row in range(startLoc[sensor]+1, endLoc[sensor]): # Determine the current tag tag = '' for col in range(1, len(data[row])): if data[row][col] == '>': tag = data[row][1:col] break # Remove the tags and newline to determine the current value data[row] = data[row].replace('<'+tag+'>', '').replace('</'+tag+'>', '') value = data[row][:-1] # Combine the current tag and value if tag == 'Location': coords = tuple(map(int, value.split(' '))) x = coords[0] + self.__model.coords[0] y = coords[1] + self.__model.coords[1] z = coords[2] + self.__model.coords[2] currentResult = {tag:(x, y, z)} elif ' ' in value: currentResult = {tag:tuple(map(float, value.split(' ')))} else: currentResult = {tag:float(value)} # Add the current tag and value to the sensor results dictionary sensorResults.update(currentResult) # Append the results dictionary for the current sensor to the simulation results list self.results.append(sensorResults) else: print('No sensors found') if os.path.exists('value_map.txt'): # Open simulation value map results f = open('value_map.txt', 'r') print('Opening file: ' + f.name) data = f.readlines() f.close() # Clear any previous results self.valueMap = np.zeros_like(self.__model.voxels, dtype=np.float32) # Get map size x_len = self.valueMap.shape[0] y_len = self.valueMap.shape[1] z_len = self.valueMap.shape[2] for z in range(z_len): # tqdm(range(z_len), desc='Loading layers'): vals = np.array(data[z][:-2].split(","), dtype=np.float32) for y in range(y_len): self.valueMap[:, y, z] = vals[y*x_len:(y+1)*x_len] # Remove temporary files if delete_files: os.remove(dirname + '/' + filename + '.vxa') os.remove(dirname + '/' + filename + '.xml') if os.path.exists(dirname + '/value_map.txt'): os.remove(dirname + '/value_map.txt') def runSimVoxCad(self, filename: str = 'temp', delete_files: bool = True, voxcad_on_path: bool = False, wsl: bool = False): """ Run a Simulation object using the VoxCad GUI. ---- Example: ``simulation = Simulation(modelResult)`` ``simulation.setCollision()`` ``simulation.setStopCondition(StopCondition.TIME_VALUE, 0.01)`` ``simulation.runSimVoxCad('collision_sim_1', delete_files=False)`` ---- Args: filename: File name delete_files: Enable/disable deleting simulation file when VoxCad is closed voxcad_on_path: Enable/disable using system VoxCad rather than bundled VoxCad wsl: Enable/disable using Windows Subsystem for Linux with bundled VoxCad Returns: None """ # Generate file name filename = filename + '_' + str(self.id) # Create simulation file self.saveVXA(filename) if voxcad_on_path: command_string = 'voxcad ' else: # Check OS type if os.name.startswith('nt'): # Windows if wsl: command_string = 'wsl "' + os.path.dirname(os.path.realpath(__file__)).replace('C:', '/mnt/c').replace('\\', '/') + '/utils/VoxCad" ' else: command_string = f'"{os.path.dirname(os.path.realpath(__file__))}\\utils\\VoxCad.exe" ' else: # Linux command_string = f'"{os.path.dirname(os.path.realpath(__file__))}/utils/VoxCad" ' command_string = command_string + filename + '.vxa' print('Launching VoxCad using: ' + command_string) p = subprocess.Popen(command_string, shell=True) p.wait() if delete_files: print('Removing file: ' + filename + '.vxa') os.remove(filename + '.vxa') def saveResults(self, filename): """ Saves a simulation's results dictionary to a .csv file. Args: filename: Name of output file Returns: None """ # Get result table keys and size keys = list(self.results[0].keys()) rows = len(self.results) cols = len(keys) # Create results file f = open(filename + '.csv', 'w+') print('Saving file: ' + f.name) # Write headings f.write('Sensor') for c in range(cols): f.write(',' + str(keys[c])) f.write('\n') # Write values for r in range(rows): f.write(str(r)) vals = list(self.results[r].values()) for c in range(cols): f.write(',' + str(vals[c]).replace(',', ' ')) f.write('\n') # Close file f.close()
Static methods
def copy(simulation)
-
Create new Simulation object with the same settings as an existing Simulation object.
Args
simulation
- Simulation to copy
Returns
Simulation
Expand source code
@classmethod def copy(cls, simulation): """ Create new Simulation object with the same settings as an existing Simulation object. Args: simulation: Simulation to copy Returns: Simulation """ # Create new simulation object and copy attribute values new_simulation = cls(simulation.__model) new_simulation.__dict__ = simulation.__dict__.copy() # Update ID and date new_simulation.id = simulation.id + 1 new_simulation.date = date.today() # Make lists copies instead of references new_simulation.__bcRegions = simulation.__bcRegions.copy() new_simulation.__sensors = simulation.__sensors.copy() new_simulation.__localTempControls = simulation.__localTempControls.copy() new_simulation.__disconnections = simulation.__disconnections.copy() new_simulation.results = simulation.results.copy() new_simulation.valueMap = simulation.valueMap.copy() return new_simulation
Methods
def addBoundaryConditionBox(self, position: Tuple[float, float, float] = (0.0, 0.0, 0.0), size: Tuple[float, float, float] = (1.0, 1.0, 0.01), fixed_dof: int = 63, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None)
-
Add a box-shaped boundary condition.
Boundary condition position and size are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied.
Args
position
- Box corner position (0-1)
size
- Box size (0-1)
fixed_dof
- Fixed degrees of freedom
force
- Force vector in N
displacement
- Displacement vector in mm
torque
- Torque values in Nm
angular_displacement
- Angular displacement values in deg
name
- Boundary condition name
Returns
None
Expand source code
def addBoundaryConditionBox(self, position: Tuple[float, float, float] = (0.0, 0.0, 0.0), size: Tuple[float, float, float] = (1.0, 1.0, 0.01), fixed_dof: int = 0b111111, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None): """ Add a box-shaped boundary condition. Boundary condition position and size are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied. Args: position: Box corner position (0-1) size: Box size (0-1) fixed_dof: Fixed degrees of freedom force: Force vector in N displacement: Displacement vector in mm torque: Torque values in Nm angular_displacement: Angular displacement values in deg name: Boundary condition name Returns: None """ bc = _BCRegion(name, BCShape.BOX, position, size, 0, fixed_dof, force, displacement, torque, angular_displacement) self.__bcRegions.append(bc)
def addBoundaryConditionCylinder(self, position: Tuple[float, float, float] = (0.45, 0.5, 0.5), axis: Axis = Axis.X, height: float = 0.1, radius: float = 0.05, fixed_dof: int = 63, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None)
-
Add a cylindrical boundary condition.
Boundary condition position and size are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied.
Args
position
- Boundary condition origin position (0-1)
axis
- Cylinder axis (0-2)
height
- Cylinder height (0-1)
radius
- Cylinder radius (0-1)
fixed_dof
- Fixed degrees of freedom
force
- Force vector in N
displacement
- Displacement vector in mm
torque
- Torque values in Nm
angular_displacement
- Angular displacement values in deg
name
- Boundary condition name
Returns
None
Expand source code
def addBoundaryConditionCylinder(self, position: Tuple[float, float, float] = (0.45, 0.5, 0.5), axis: Axis = Axis.X, height: float = 0.1, radius: float = 0.05, fixed_dof: int = 0b111111, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None): """ Add a cylindrical boundary condition. Boundary condition position and size are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied. Args: position: Boundary condition origin position (0-1) axis: Cylinder axis (0-2) height: Cylinder height (0-1) radius: Cylinder radius (0-1) fixed_dof: Fixed degrees of freedom force: Force vector in N displacement: Displacement vector in mm torque: Torque values in Nm angular_displacement: Angular displacement values in deg name: Boundary condition name Returns: None """ size = [0.0, 0.0, 0.0] size[axis.value] = height bc = _BCRegion(name, BCShape.CYLINDER, position, (size[0], size[1], size[2]), radius, fixed_dof, force, displacement, torque, angular_displacement) self.__bcRegions.append(bc)
def addBoundaryConditionSphere(self, position: Tuple[float, float, float] = (0.5, 0.5, 0.5), radius: float = 0.05, fixed_dof: int = 63, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None)
-
Add a spherical boundary condition.
Boundary condition position and radius are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied.
Args
position
- Sphere center position (0-1)
radius
- Sphere radius (0-1)
fixed_dof
- Fixed degrees of freedom
force
- Force vector in N
displacement
- Displacement vector in mm
torque
- Torque values in Nm
angular_displacement
- Angular displacement values in deg
name
- Boundary condition name
Returns
None
Expand source code
def addBoundaryConditionSphere(self, position: Tuple[float, float, float] = (0.5, 0.5, 0.5), radius: float = 0.05, fixed_dof: int = 0b111111, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None): """ Add a spherical boundary condition. Boundary condition position and radius are expressed as percentages of the overall model size. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied. Args: position: Sphere center position (0-1) radius: Sphere radius (0-1) fixed_dof: Fixed degrees of freedom force: Force vector in N displacement: Displacement vector in mm torque: Torque values in Nm angular_displacement: Angular displacement values in deg name: Boundary condition name Returns: None """ bc = _BCRegion(name, BCShape.SPHERE, position, (0.0, 0.0, 0.0), radius, fixed_dof, force, displacement, torque, angular_displacement) self.__bcRegions.append(bc)
def addBoundaryConditionVoxel(self, position: Tuple[int, int, int] = (0, 0, 0), fixed_dof: int = 63, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None)
-
Add a boundary condition at a specific voxel.
The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied.
Args
position
- Position in voxels
fixed_dof
- Fixed degrees of freedom
force
- Force vector in N
displacement
- Displacement vector in mm
torque
- Torque values in Nm
angular_displacement
- Angular displacement values in deg
name
- Boundary condition name
Returns
None
Expand source code
def addBoundaryConditionVoxel(self, position: Tuple[int, int, int] = (0, 0, 0), fixed_dof: int = 0b111111, force: Tuple[float, float, float] = (0, 0, 0), displacement: Tuple[float, float, float] = (0, 0, 0), torque: Tuple[float, float, float] = (0, 0, 0), angular_displacement: Tuple[float, float, float] = (0, 0, 0), name: str = None): """ Add a boundary condition at a specific voxel. The fixed DOF value should be set as a 6-bit binary value (e.g. 0b111111) and the bits correspond to: Rz, Ry, Rx, Z, Y, X. If a bit is set to 0, the corresponding force/torque will be applied. If a bit is set to 1, the DOF will be fixed and the displacement will be applied. Args: position: Position in voxels fixed_dof: Fixed degrees of freedom force: Force vector in N displacement: Displacement vector in mm torque: Torque values in Nm angular_displacement: Angular displacement values in deg name: Boundary condition name Returns: None """ x = position[0] - self.__model.coords[0] y = position[1] - self.__model.coords[1] z = position[2] - self.__model.coords[2] x_len = int(self.__model.voxels.shape[0]) y_len = int(self.__model.voxels.shape[1]) z_len = int(self.__model.voxels.shape[2]) pos = ((x+0.5)/x_len, (y+0.5)/y_len, (z+0.5)/z_len) radius = 0.49/x_len bc = _BCRegion(name, BCShape.SPHERE, pos, (0.0, 0.0, 0.0), radius, fixed_dof, force, displacement, torque, angular_displacement) self.__bcRegions.append(bc)
def addDisconnection(self, voxel_1: Tuple[int, int, int], voxel_2: Tuple[int, int, int])
-
Specify a pair of voxels which should be disconnected
Args
voxel_1
- Coordinates in voxels
voxel_2
- Coordinates in voxels
Returns
None
Expand source code
def addDisconnection(self, voxel_1: Tuple[int, int, int], voxel_2: Tuple[int, int, int]): """ Specify a pair of voxels which should be disconnected Args: voxel_1: Coordinates in voxels voxel_2: Coordinates in voxels Returns: None """ dc = _Disconnection(voxel_1, voxel_2) self.__disconnections.append(dc)
def addKeyframe(self, time_value: float = 0, amplitude_pos: float = 0, amplitude_neg: float = -1, percent_pos: float = 0.5, period: float = 1.0, phase_offset: float = 0, temp_offset: float = 0, const_temp: bool = False, square_wave: bool = False)
-
Add a keyframe to a temperature control group.
Args
time_value
- Time at which keyframe should take effect (sec)
amplitude_pos
- Control element positive temperature amplitude (deg C)
amplitude_neg
- Control element negative temperature amplitude (deg C)
percent_pos
- Percent of period spanned by positive temperature amplitude (0-1)
period
- Period of the control signal (sec)
phase_offset
- Control element phase offset for time-varying thermal (rad)
temp_offset
- Control element temperature offset for time-varying thermal (deg C)
const_temp
- Enable/disable setting a constant target temperature that respects heating/cooling rates
square_wave
- Enable/disable converting signal to a square wave (positive -> a = amplitude1, negative -> a = 0)
Returns
None
Expand source code
def addKeyframe(self, time_value: float = 0, amplitude_pos: float = 0, amplitude_neg: float = -1, percent_pos: float = 0.5, period: float = 1.0, phase_offset: float = 0, temp_offset: float = 0, const_temp: bool = False, square_wave: bool = False): """ Add a keyframe to a temperature control group. Args: time_value: Time at which keyframe should take effect (sec) amplitude_pos: Control element positive temperature amplitude (deg C) amplitude_neg: Control element negative temperature amplitude (deg C) percent_pos: Percent of period spanned by positive temperature amplitude (0-1) period: Period of the control signal (sec) phase_offset: Control element phase offset for time-varying thermal (rad) temp_offset: Control element temperature offset for time-varying thermal (deg C) const_temp: Enable/disable setting a constant target temperature that respects heating/cooling rates square_wave: Enable/disable converting signal to a square wave (positive -> a = amplitude1, negative -> a = 0) Returns: None """ amplitude_pos = max(amplitude_pos, 0) # amplitude_pos must be positive if amplitude_neg < 0: # If amplitude neg is not given (or not negative) use amplitude_pos instead amplitude_neg = amplitude_pos g = self.__currentTempControlGroup kf = _Keyframe(time_value, amplitude_pos, amplitude_neg, percent_pos, period, phase_offset, temp_offset, const_temp, square_wave) self.__localTempControls[g].keyframes.append(kf)
def addSensor(self, location: Tuple[int, int, int] = (0, 0, 0), axis: Axis = Axis.NONE, name: str = None)
-
Add a sensor to a voxel.
Args
location
- Sensor location in voxels
axis
- Sensor measurement axis
name
- Sensor name
Returns
None
Expand source code
def addSensor(self, location: Tuple[int, int, int] = (0, 0, 0), axis: Axis = Axis.NONE, name: str = None): # TODO: Make Voxelyze use axis parameter """ Add a sensor to a voxel. Args: location: Sensor location in voxels axis: Sensor measurement axis name: Sensor name Returns: None """ x = location[0] - self.__model.coords[0] y = location[1] - self.__model.coords[1] z = location[2] - self.__model.coords[2] sensor = _Sensor(name, (x, y, z), axis) self.__sensors.append(sensor) if not self.__model.isOccupied((x, y, z)): print('WARNING: No material present at sensor voxel ' + str((x, y, z)))
def addTempControlGroup(self, locations: List[Tuple[int, int, int]] = None, name: str = None)
-
Add a new temperature control group and select it.
Args
locations
- Control element locations in voxels as a list of tuples
name
- Group name
Returns
None
Expand source code
def addTempControlGroup(self, locations: List[Tuple[int, int, int]] = None, name: str = None): """ Add a new temperature control group and select it. Args: locations: Control element locations in voxels as a list of tuples name: Group name Returns: None """ # Enable temperature control self.__temperatureEnable = True self.__temperatureVaryEnable = True if locations is None: print('No locations provided - applying temperature control group to entire model') x_len = self.__model.voxels.shape[0] y_len = self.__model.voxels.shape[1] z_len = self.__model.voxels.shape[2] locations = [] for x in range(x_len): for y in range(y_len): for z in range(z_len): locations.append((x, y, z)) group = _TempControlGroup(name, locations, []) self.__localTempControls.append(group) self.__currentTempControlGroup = len(self.__localTempControls)-1
def applyTempMap(self, time_value, amp_pos_map, amp_neg_map=None, percent_pos_map=None, period_map=None, phase_map=None, offset_map=None, const_temp_map=None, square_wave_map=None)
-
Set the simulation temperature control elements based on a value maps of target temperature settings.
This function relies on the temperature control groups being in a specific order. initializeTempMap should be called prior to running this function.
Args
time_value
- Time at which keyframe should take effect (sec)
amp_pos_map
- Array of target positive temperature amplitudes for each voxel (deg C)
amp_neg_map
- Array of target negative temperature amplitudes for each voxel (deg C)
percent_pos_map
- Array containing percent of period spanned by positive temperature amplitude for each voxel
period_map
- Array of signal periods for each voxel (sec)
phase_map
- Array of phase offsets for each voxel (rad)
offset_map
- Array of temperature offsets for each voxel (deg C)
const_temp_map
- Array of boolean values to enable/disable constant temperature mode
square_wave_map
- Array of boolean values to enable/disable square wave mode
Returns
None
Expand source code
def applyTempMap(self, time_value, amp_pos_map, amp_neg_map=None, percent_pos_map=None, period_map=None, phase_map=None, offset_map=None, const_temp_map=None, square_wave_map=None): """ Set the simulation temperature control elements based on a value maps of target temperature settings. This function relies on the temperature control groups being in a specific order. initializeTempMap should be called prior to running this function. Args: time_value: Time at which keyframe should take effect (sec) amp_pos_map: Array of target positive temperature amplitudes for each voxel (deg C) amp_neg_map: Array of target negative temperature amplitudes for each voxel (deg C) percent_pos_map: Array containing percent of period spanned by positive temperature amplitude for each voxel period_map: Array of signal periods for each voxel (sec) phase_map: Array of phase offsets for each voxel (rad) offset_map: Array of temperature offsets for each voxel (deg C) const_temp_map: Array of boolean values to enable/disable constant temperature mode square_wave_map: Array of boolean values to enable/disable square wave mode Returns: None """ # Get map size x_len = amp_pos_map.shape[0] y_len = amp_pos_map.shape[1] z_len = amp_pos_map.shape[2] # Generate the control element for each voxel for x in range(x_len): for y in range(y_len): for z in range(z_len): if abs(amp_pos_map[x, y, z]) > FLOATING_ERROR or ((amp_neg_map is not None) and (abs(amp_neg_map[x, y, z]) > FLOATING_ERROR)): # If voxel is not empty amplitude_pos = amp_pos_map[x, y, z] if amp_neg_map is None: amplitude_neg = amp_pos_map[x, y, z] else: amplitude_neg = amp_neg_map[x, y, z] if percent_pos_map is None: percent_pos = 0.5 else: percent_pos = percent_pos_map[x, y, z] if period_map is None: period = 1.0 else: period = period_map[x, y, z] if phase_map is None: phase_offset = 0 else: phase_offset = phase_map[x, y, z] if offset_map is None: temp_offset = 0 else: temp_offset = offset_map[x, y, z] if const_temp_map is None: const_temp = False else: const_temp = const_temp_map[x, y, z] if square_wave_map is None: square_wave = False else: square_wave = square_wave_map[x, y, z] kf = _Keyframe(time_value, amplitude_pos, amplitude_neg, percent_pos, period, phase_offset, temp_offset, const_temp, square_wave) g = z + y*z_len + x*y_len*z_len self.__localTempControls[g].keyframes.append(kf)
def cleanTempControlGroups(self)
-
Remove invalid temperature control groups.
Invalid groups have no keyframes, or have no target voxels.
Returns
Number of groups removed
Expand source code
def cleanTempControlGroups(self): """ Remove invalid temperature control groups. Invalid groups have no keyframes, or have no target voxels. Returns: Number of groups removed """ removed_groups = [] for g in range(len(self.__localTempControls)): g = g - len(removed_groups) group = self.__localTempControls[g] if len(group.keyframes) == 0 or len(group.locations) == 0: removed_groups.append(self.removeTempControlGroup(g)) self.__currentTempControlGroup = len(self.__localTempControls)-1 named_groups = [] for n in removed_groups: if n is not None: named_groups.append(n) if len(removed_groups) == 0: pass elif len(named_groups) == 0: print('Removed ' + str(len(removed_groups)) + ' temperature control groups') else: print('Removed ' + str(len(removed_groups)) + ' temperature control groups, including: ' + str(named_groups)) return len(removed_groups)
def clearBoundaryConditions(self)
-
Remove all boundary conditions from a Simulation object.
Returns
None
Expand source code
def clearBoundaryConditions(self): """ Remove all boundary conditions from a Simulation object. Returns: None """ self.__bcRegions = [] # self.__bcVoxels = []
def clearDisconnections(self)
-
Clear all disconnected voxel bonds.
Returns
None
Expand source code
def clearDisconnections(self): """ Clear all disconnected voxel bonds. Returns: None """ self.__disconnections = []
def clearKeyframes(self)
-
Remove all keyframes assigned to the current temperature control group.
Returns
None
Expand source code
def clearKeyframes(self): """ Remove all keyframes assigned to the current temperature control group. Returns: None """ g = self.__currentTempControlGroup self.__localTempControls[g].keyframes = []
def clearSensors(self)
-
Remove all sensors from a Simulation object.
Returns
None
Expand source code
def clearSensors(self): """ Remove all sensors from a Simulation object. Returns: None """ self.__sensors = []
def clearTempControlGroups(self)
-
Clear all temperature control groups.
Returns
None
Expand source code
def clearTempControlGroups(self): """ Clear all temperature control groups. Returns: None """ self.__currentTempControlGroup = 0 self.__localTempControls = []
def getCollision(self)
-
Get simulation collision parameters.
Returns
Enable/disable collisions, Collision damping
Expand source code
def getCollision(self): """ Get simulation collision parameters. Returns: Enable/disable collisions, Collision damping """ return self.__collisionEnable, self.__collisionDamping
def getDamping(self)
-
Get simulation damping parameters.
Returns
Voxel bond damping, Environment damping
Expand source code
def getDamping(self): """ Get simulation damping parameters. Returns: Voxel bond damping, Environment damping """ return self.__dampingBond, self.__dampingEnvironment
def getEquilibriumMode(self)
-
Get simulation equilibrium mode.
Returns
Enable/disable equilibrium mode
Expand source code
def getEquilibriumMode(self): """ Get simulation equilibrium mode. Returns: Enable/disable equilibrium mode """ return self.__equilibriumModeEnable
def getGravity(self)
-
Get simulation gravity parameters.
Returns
Enable/disable gravity, Acceleration due to gravity in m/sec^2, Enable/disable ground plane
Expand source code
def getGravity(self): """ Get simulation gravity parameters. Returns: Enable/disable gravity, Acceleration due to gravity in m/sec^2, Enable/disable ground plane """ return self.__gravityEnable, self.__gravityValue, self.__floorEnable
def getHydrogelModel(self)
-
Get hydrogel model parameters.
Returns
Enable/disable hydrogel model
Expand source code
def getHydrogelModel(self): """ Get hydrogel model parameters. Returns: Enable/disable hydrogel model """ return self.__hydrogelModelEnable
def getModel(self)
-
Get the simulation model.
Returns
VoxelModel
Expand source code
def getModel(self): """ Get the simulation model. Returns: VoxelModel """ return self.__model
def getStopCondition(self)
-
Get simulation stop condition.
Returns
Stop condition type, Stop condition value
Expand source code
def getStopCondition(self): """ Get simulation stop condition. Returns: Stop condition type, Stop condition value """ return self.__stopConditionType, self.__stopConditionValue
def getThermal(self)
-
Get simulation temperature parameters.
Returns
Enable/disable temperature, Base temperature in degrees C, Enable/disable temperature fluctuation, Temperature fluctuation amplitude, Temperature fluctuation period, Temperature offset
Expand source code
def getThermal(self): """ Get simulation temperature parameters. Returns: Enable/disable temperature, Base temperature in degrees C, Enable/disable temperature fluctuation, Temperature fluctuation amplitude, Temperature fluctuation period, Temperature offset """ return self.__temperatureEnable, self.__temperatureBaseValue, self.__temperatureVaryEnable, self.__temperatureVaryAmplitude, self.__temperatureVaryPeriod, self.__temperatureVaryOffset
def getVoxelDim(self)
-
Get the side dimension of a voxel in mm.
Returns
Float
Expand source code
def getVoxelDim(self): """ Get the side dimension of a voxel in mm. Returns: Float """ res = self.__model.resolution return (1.0/res) * 0.001
def initializeTempMap(self)
-
Initialize temperature control groups to which keyframes will be stored when using applyTempMap.
Returns
None
Expand source code
def initializeTempMap(self): """ Initialize temperature control groups to which keyframes will be stored when using applyTempMap. Returns: None """ # Clear any existing temperature controls self.clearTempControlGroups() # Get map size x_len = self.__model.voxels.shape[0] y_len = self.__model.voxels.shape[1] z_len = self.__model.voxels.shape[2] # Generate empty control element for each voxel for x in range(x_len): for y in range(y_len): for z in range(z_len): self.addTempControlGroup([(x, y, z)])
def removeTempControlGroup(self, index: int = 0)
-
Remove a temperature control group by index.
Args
index
- Temperature control group index
Returns
Name of removed group
Expand source code
def removeTempControlGroup(self, index: int = 0): """ Remove a temperature control group by index. Args: index: Temperature control group index Returns: Name of removed group """ group = self.__localTempControls.pop(index) self.__currentTempControlGroup = len(self.__localTempControls)-1 return group.name
def runSim(self, filename: str = 'temp', value_map: int = 0, delete_files: bool = True, log_interval: int = -1, history_interval: int = -1, voxelyze_on_path: bool = False, wsl: bool = False)
-
Run a Simulation object using Voxelyze.
This function will create a .vxa file, run the file with Voxelyze, and then load the .xml results file into the results attribute of the Simulation object. Enabling delete_files will delete both the .vxa and .xml files once the results have been loaded.
History files can be viewed using https://github.com/voxcraft/voxcraft-viz
Args
filename
- File name for .vxa and .xml files
value_map
- Index of the desired value map type
log_interval
- Set the step interval at which sensor log entries should be recorded, -1 to disable log
history_interval
- Set the step interval at which history file entries should be recorded, -1 for default interval
delete_files
- Enable/disable deleting simulation file when process is complete
voxelyze_on_path
- Enable/disable using system Voxelyze rather than bundled Voxelyze
wsl
- Enable/disable using Windows Subsystem for Linux with bundled Voxelyze
Returns
None
Expand source code
def runSim(self, filename: str = 'temp', value_map: int = 0, delete_files: bool = True, log_interval: int = -1, history_interval: int = -1, voxelyze_on_path: bool = False, wsl: bool = False): """ Run a Simulation object using Voxelyze. This function will create a .vxa file, run the file with Voxelyze, and then load the .xml results file into the results attribute of the Simulation object. Enabling delete_files will delete both the .vxa and .xml files once the results have been loaded. History files can be viewed using https://github.com/voxcraft/voxcraft-viz Args: filename: File name for .vxa and .xml files value_map: Index of the desired value map type log_interval: Set the step interval at which sensor log entries should be recorded, -1 to disable log history_interval: Set the step interval at which history file entries should be recorded, -1 for default interval delete_files: Enable/disable deleting simulation file when process is complete voxelyze_on_path: Enable/disable using system Voxelyze rather than bundled Voxelyze wsl: Enable/disable using Windows Subsystem for Linux with bundled Voxelyze Returns: None """ # Generate results file/directory names filename = filename + '_' + str(self.id) dirname = 'sim_results_' + str(self.date) # Create results directory if not os.path.exists(dirname): os.makedirs(dirname) # Create simulation file self.saveVXA(dirname + '/' + filename) if voxelyze_on_path: command_string = 'voxelyze' else: # Check OS type if os.name.startswith('nt'): # Windows if wsl: command_string = 'wsl "' + os.path.dirname(os.path.realpath(__file__)).replace('C:', '/mnt/c').replace('\\', '/') + '/utils/voxelyze"' else: command_string = f'"{os.path.dirname(os.path.realpath(__file__))}\\utils\\voxelyze.exe"' else: # Linux command_string = f'"{os.path.dirname(os.path.realpath(__file__))}/utils/voxelyze"' command_string = command_string + ' -f ' + dirname + '/' + filename + '.vxa -o ' + dirname + '/' + filename + ' -vm ' + str(value_map) + ' -p' if log_interval > 0: command_string = command_string + ' -log-interval ' + str(log_interval) if history_interval > 0: command_string = command_string + ' -history-interval ' + str(history_interval) print('Launching Voxelyze using: ' + command_string) p = subprocess.Popen(command_string, shell=True) p.wait() # Open simulation results f = open(dirname + '/' + filename + '.xml', 'r') #print('Opening file: ' + f.name) data = f.readlines() f.close() # Clear any previous results self.results = [] # Find start and end locations for individual sensors startLoc = [] endLoc = [] for row in range(len(data)): # tqdm(range(len(data)), desc='Finding sensor tags'): data[row] = data[row].replace('\t', '') if data[row][:-1] == '<Sensor>': startLoc.append(row) elif data[row][:-1] == '</Sensor>': endLoc.append(row) # Read the data from each sensor sensor_count = len(startLoc) if sensor_count > 0: for sensor in range(sensor_count): # tqdm(range(len(startLoc)), desc='Reading sensor results'): # Create a dictionary to hold the current sensor results sensorResults = {} # Read the data from each sensor tag for row in range(startLoc[sensor]+1, endLoc[sensor]): # Determine the current tag tag = '' for col in range(1, len(data[row])): if data[row][col] == '>': tag = data[row][1:col] break # Remove the tags and newline to determine the current value data[row] = data[row].replace('<'+tag+'>', '').replace('</'+tag+'>', '') value = data[row][:-1] # Combine the current tag and value if tag == 'Location': coords = tuple(map(int, value.split(' '))) x = coords[0] + self.__model.coords[0] y = coords[1] + self.__model.coords[1] z = coords[2] + self.__model.coords[2] currentResult = {tag:(x, y, z)} elif ' ' in value: currentResult = {tag:tuple(map(float, value.split(' ')))} else: currentResult = {tag:float(value)} # Add the current tag and value to the sensor results dictionary sensorResults.update(currentResult) # Append the results dictionary for the current sensor to the simulation results list self.results.append(sensorResults) else: print('No sensors found') if os.path.exists('value_map.txt'): # Open simulation value map results f = open('value_map.txt', 'r') print('Opening file: ' + f.name) data = f.readlines() f.close() # Clear any previous results self.valueMap = np.zeros_like(self.__model.voxels, dtype=np.float32) # Get map size x_len = self.valueMap.shape[0] y_len = self.valueMap.shape[1] z_len = self.valueMap.shape[2] for z in range(z_len): # tqdm(range(z_len), desc='Loading layers'): vals = np.array(data[z][:-2].split(","), dtype=np.float32) for y in range(y_len): self.valueMap[:, y, z] = vals[y*x_len:(y+1)*x_len] # Remove temporary files if delete_files: os.remove(dirname + '/' + filename + '.vxa') os.remove(dirname + '/' + filename + '.xml') if os.path.exists(dirname + '/value_map.txt'): os.remove(dirname + '/value_map.txt')
def runSimVoxCad(self, filename: str = 'temp', delete_files: bool = True, voxcad_on_path: bool = False, wsl: bool = False)
-
Run a Simulation object using the VoxCad GUI.
Example:
simulation = Simulation(modelResult)
simulation.setCollision()
simulation.setStopCondition(StopCondition.TIME_VALUE, 0.01)
simulation.runSimVoxCad('collision_sim_1', delete_files=False)
Args
filename
- File name
delete_files
- Enable/disable deleting simulation file when VoxCad is closed
voxcad_on_path
- Enable/disable using system VoxCad rather than bundled VoxCad
wsl
- Enable/disable using Windows Subsystem for Linux with bundled VoxCad
Returns
None
Expand source code
def runSimVoxCad(self, filename: str = 'temp', delete_files: bool = True, voxcad_on_path: bool = False, wsl: bool = False): """ Run a Simulation object using the VoxCad GUI. ---- Example: ``simulation = Simulation(modelResult)`` ``simulation.setCollision()`` ``simulation.setStopCondition(StopCondition.TIME_VALUE, 0.01)`` ``simulation.runSimVoxCad('collision_sim_1', delete_files=False)`` ---- Args: filename: File name delete_files: Enable/disable deleting simulation file when VoxCad is closed voxcad_on_path: Enable/disable using system VoxCad rather than bundled VoxCad wsl: Enable/disable using Windows Subsystem for Linux with bundled VoxCad Returns: None """ # Generate file name filename = filename + '_' + str(self.id) # Create simulation file self.saveVXA(filename) if voxcad_on_path: command_string = 'voxcad ' else: # Check OS type if os.name.startswith('nt'): # Windows if wsl: command_string = 'wsl "' + os.path.dirname(os.path.realpath(__file__)).replace('C:', '/mnt/c').replace('\\', '/') + '/utils/VoxCad" ' else: command_string = f'"{os.path.dirname(os.path.realpath(__file__))}\\utils\\VoxCad.exe" ' else: # Linux command_string = f'"{os.path.dirname(os.path.realpath(__file__))}/utils/VoxCad" ' command_string = command_string + filename + '.vxa' print('Launching VoxCad using: ' + command_string) p = subprocess.Popen(command_string, shell=True) p.wait() if delete_files: print('Removing file: ' + filename + '.vxa') os.remove(filename + '.vxa')
def saveResults(self, filename)
-
Saves a simulation's results dictionary to a .csv file.
Args
filename
- Name of output file
Returns
None
Expand source code
def saveResults(self, filename): """ Saves a simulation's results dictionary to a .csv file. Args: filename: Name of output file Returns: None """ # Get result table keys and size keys = list(self.results[0].keys()) rows = len(self.results) cols = len(keys) # Create results file f = open(filename + '.csv', 'w+') print('Saving file: ' + f.name) # Write headings f.write('Sensor') for c in range(cols): f.write(',' + str(keys[c])) f.write('\n') # Write values for r in range(rows): f.write(str(r)) vals = list(self.results[r].values()) for c in range(cols): f.write(',' + str(vals[c]).replace(',', ' ')) f.write('\n') # Close file f.close()
def saveVXA(self, filename: str, compression: bool = False)
-
Save model data to a .vxa file
The VoxCad simulation file format stores all the data contained in a .vxc file (geometry, material palette) plus the simulation setup (simulation parameters, environment settings, boundary conditions).
This format supports compression for the voxel data. Enabling compression allows for larger models, but it may introduce geometry errors that particularly affect small models.
The .vxa file type can be opened using VoxCad simulation software. However, it cannot currently be reopened by a VoxelFuse script.
Args
filename
- File name
compression
- Enable/disable voxel data compression
Returns
None
Expand source code
def saveVXA(self, filename: str, compression: bool = False): """ Save model data to a .vxa file The VoxCad simulation file format stores all the data contained in a .vxc file (geometry, material palette) plus the simulation setup (simulation parameters, environment settings, boundary conditions). This format supports compression for the voxel data. Enabling compression allows for larger models, but it may introduce geometry errors that particularly affect small models. The .vxa file type can be opened using VoxCad simulation software. However, it cannot currently be reopened by a VoxelFuse script. Args: filename: File name compression: Enable/disable voxel data compression Returns: None """ self.cleanTempControlGroups() f = open(filename + '.vxa', 'w+') print('Saving file: ' + f.name) writeHeader(f, '1.0', 'ISO-8859-1') writeOpen(f, 'VXA Version="' + str(1.1) + '"', 0) self.writeSimData(f) self.writeEnvironmentData(f) self.writeSensors(f) self.writeTempControls(f) self.writeDisconnections(f) self.__model.writeVXCData(f, compression) writeClos(f, 'VXA', 0) f.close()
def selectTempControlGroup(self, index: int = 0)
-
Select which keyframe new temperature control elements should be added to.
Args
index
- Temperature control group index
Returns
None
Expand source code
def selectTempControlGroup(self, index: int = 0): """ Select which keyframe new temperature control elements should be added to. Args: index: Temperature control group index Returns: None """ self.__currentTempControlGroup = index
def setCollision(self, enable: bool = True, damping: float = 1.0)
-
Set simulation collision parameters.
A damping value of 0 represents completely elastic collisions and higher values represent inelastic collisions.
Args
enable
- Enable/disable collisions
damping
- Collision damping (0-2)
Returns
None
Expand source code
def setCollision(self, enable: bool = True, damping: float = 1.0): """ Set simulation collision parameters. A damping value of 0 represents completely elastic collisions and higher values represent inelastic collisions. Args: enable: Enable/disable collisions damping: Collision damping (0-2) Returns: None """ self.__collisionEnable = enable self.__collisionDamping = damping
def setDamping(self, bond: float = 1.0, environment: float = 0.0001)
-
Set simulation damping parameters.
Environment damping can be used to simulate fluid environments. 0 represents a vacuum and larger values represent a viscous fluid.
Args
bond
- Voxel bond damping (0-1)
environment
- Environment damping (0-0.1)
Returns
None
Expand source code
def setDamping(self, bond: float = 1.0, environment: float = 0.0001): """ Set simulation damping parameters. Environment damping can be used to simulate fluid environments. 0 represents a vacuum and larger values represent a viscous fluid. Args: bond: Voxel bond damping (0-1) environment: Environment damping (0-0.1) Returns: None """ self.__dampingBond = bond self.__dampingEnvironment = environment
def setEquilibriumMode(self, enable: bool = True)
-
Set simulation equilibrium mode.
Args
enable
- Enable/disable equilibrium mode
Returns
None
Expand source code
def setEquilibriumMode(self, enable: bool = True): """ Set simulation equilibrium mode. Args: enable: Enable/disable equilibrium mode Returns: None """ self.__equilibriumModeEnable = enable
def setFixedThermal(self, enable: bool = True, base_temp: float = 25.0, growth_amplitude: float = 0.0)
-
Set a fixed environment temperature.
Args
enable
- Enable/disable temperature
base_temp
- Temperature in degrees C
growth_amplitude
- Set to 1 to enable expansion from base size
Returns
None
Expand source code
def setFixedThermal(self, enable: bool = True, base_temp: float = 25.0, growth_amplitude: float = 0.0): """ Set a fixed environment temperature. Args: enable: Enable/disable temperature base_temp: Temperature in degrees C growth_amplitude: Set to 1 to enable expansion from base size Returns: None """ self.__temperatureEnable = enable self.__temperatureBaseValue = base_temp self.__temperatureVaryEnable = False self.__growthAmplitude = growth_amplitude
def setGravity(self, enable: bool = True, value: float = -9.81, enable_floor: bool = True)
-
Set simulation gravity parameters.
Args
enable
- Enable/disable gravity
value
- Acceleration due to gravity in m/sec^2
enable_floor
- Enable/disable ground plane
Returns
None
Expand source code
def setGravity(self, enable: bool = True, value: float = -9.81, enable_floor: bool = True): """ Set simulation gravity parameters. Args: enable: Enable/disable gravity value: Acceleration due to gravity in m/sec^2 enable_floor: Enable/disable ground plane Returns: None """ self.__gravityEnable = enable self.__gravityValue = value self.__floorEnable = enable_floor
def setHydrogelModel(self, enable: bool = True)
-
Set hydrogel model parameters.
Args
enable
- Enable/disable hydrogel model
Returns
None
Expand source code
def setHydrogelModel(self, enable: bool = True): """ Set hydrogel model parameters. Args: enable: Enable/disable hydrogel model Returns: None """ self.__hydrogelModelEnable = enable
def setModel(self, voxel_model)
-
Set the model for a simulation.
Models located at positive coordinate values will have their workspace size adjusted to maintain their position in the exported simulation. Models located at negative coordinate values will be shifted to the origin.
Args
voxel_model
- VoxelModel
Returns
None
Expand source code
def setModel(self, voxel_model): """ Set the model for a simulation. Models located at positive coordinate values will have their workspace size adjusted to maintain their position in the exported simulation. Models located at negative coordinate values will be shifted to the origin. Args: voxel_model: VoxelModel Returns: None """ # Fit workspace and union with an empty object at the origin to clear offsets if object is raised self.__model = ((VoxelModel.copy(voxel_model).fitWorkspace()) | empty(num_materials=(voxel_model.materials.shape[1] - 1))).removeDuplicateMaterials()
def setStopCondition(self, condition: StopCondition = StopCondition.NONE, value: float = 0)
-
Set simulation stop condition.
Args
condition
- Stop condition type, set using StopCondition class
value
- Stop condition value
Returns
None
Expand source code
def setStopCondition(self, condition: StopCondition = StopCondition.NONE, value: float = 0): """ Set simulation stop condition. Args: condition: Stop condition type, set using StopCondition class value: Stop condition value Returns: None """ self.__stopConditionType = condition self.__stopConditionValue = value
def setVaryingThermal(self, enable: bool = True, base_temp: float = 25.0, amplitude: float = 0.0, period: float = 1.0, offset: float = 0.0, growth_amplitude: float = 1.0)
-
Set a varying environment temperature.
Args
enable
- Enable/disable temperature
base_temp
- Base temperature in degrees C
amplitude
- Temperature fluctuation amplitude
period
- Temperature fluctuation period
offset
- Temperature offset (not currently supported)
growth_amplitude
- Set to 1 to enable expansion from base size
Returns
None
Expand source code
def setVaryingThermal(self, enable: bool = True, base_temp: float = 25.0, amplitude: float = 0.0, period: float = 1.0, offset: float = 0.0, growth_amplitude: float = 1.0): """ Set a varying environment temperature. Args: enable: Enable/disable temperature base_temp: Base temperature in degrees C amplitude: Temperature fluctuation amplitude period: Temperature fluctuation period offset: Temperature offset (not currently supported) growth_amplitude: Set to 1 to enable expansion from base size Returns: None """ self.__temperatureEnable = enable self.__temperatureBaseValue = base_temp self.__temperatureVaryEnable = enable self.__temperatureVaryAmplitude = amplitude self.__temperatureVaryPeriod = period self.__temperatureVaryOffset = offset self.__growthAmplitude = growth_amplitude
def writeDisconnections(self, f:
) -
Write bond disconnections to a text file using the .vxa format.
Args
f
- File to write to
Returns
None
Expand source code
def writeDisconnections(self, f: TextIO): """ Write bond disconnections to a text file using the .vxa format. Args: f: File to write to Returns: None """ writeOpen(f, 'Disconnections', 0) for dc in self.__disconnections: writeOpen(f, 'Break', 1) writeData(f, 'Voxel1', str(dc.vx1).replace('(', '').replace(',', '').replace(')', ''), 2) writeData(f, 'Voxel2', str(dc.vx2).replace('(', '').replace(',', '').replace(')', ''), 2) writeClos(f, 'Break', 1) writeClos(f, 'Disconnections', 0)
def writeEnvironmentData(self, f:
) -
Write simulation environment parameters to a text file using the .vxa format.
Args
f
- File to write to
Returns
None
Expand source code
def writeEnvironmentData(self, f: TextIO): """ Write simulation environment parameters to a text file using the .vxa format. Args: f: File to write to Returns: None """ # Environment settings writeOpen(f, 'Environment', 0) writeOpen(f, 'Boundary_Conditions', 1) writeData(f, 'NumBCs', len(self.__bcRegions), 2) for bc in self.__bcRegions: writeOpen(f, 'FRegion', 2) if bc.name is not None: writeData(f, 'Name', bc.name, 3) writeData(f, 'PrimType', int(bc.shape.value), 3) writeData(f, 'X', bc.position[0], 3) writeData(f, 'Y', bc.position[1], 3) writeData(f, 'Z', bc.position[2], 3) writeData(f, 'dX', bc.size[0], 3) writeData(f, 'dY', bc.size[1], 3) writeData(f, 'dZ', bc.size[2], 3) writeData(f, 'Radius', bc.radius, 3) writeData(f, 'R', bc.color[0], 3) writeData(f, 'G', bc.color[1], 3) writeData(f, 'B', bc.color[2], 3) writeData(f, 'alpha', bc.color[3], 3) writeData(f, 'DofFixed', bc.fixed_dof, 3) writeData(f, 'ForceX', bc.force[0], 3) writeData(f, 'ForceY', bc.force[1], 3) writeData(f, 'ForceZ', bc.force[2], 3) writeData(f, 'TorqueX', bc.torque[0], 3) writeData(f, 'TorqueY', bc.torque[1], 3) writeData(f, 'TorqueZ', bc.torque[2], 3) writeData(f, 'DisplaceX', bc.displacement[0] * 1e-3, 3) writeData(f, 'DisplaceY', bc.displacement[1] * 1e-3, 3) writeData(f, 'DisplaceZ', bc.displacement[2] * 1e-3, 3) writeData(f, 'AngDisplaceX', bc.angular_displacement[0], 3) writeData(f, 'AngDisplaceY', bc.angular_displacement[1], 3) writeData(f, 'AngDisplaceZ', bc.angular_displacement[2], 3) writeClos(f, 'FRegion', 2) writeClos(f, 'Boundary_Conditions', 1) writeOpen(f, 'Gravity', 1) writeData(f, 'GravEnabled', int(self.__gravityEnable), 2) writeData(f, 'GravAcc', self.__gravityValue, 2) writeData(f, 'FloorEnabled', int(self.__floorEnable), 2) writeClos(f, 'Gravity', 1) writeOpen(f, 'Thermal', 1) writeData(f, 'TempEnabled', int(self.__temperatureEnable), 2) writeData(f, 'TempAmplitude', self.__temperatureVaryAmplitude, 2) writeData(f, 'TempBase', self.__temperatureBaseValue, 2) writeData(f, 'VaryTempEnabled', int(self.__temperatureVaryEnable), 2) writeData(f, 'TempPeriod', self.__temperatureVaryPeriod, 2) writeData(f, 'TempOffset', self.__temperatureVaryOffset, 2) writeClos(f, 'Thermal', 1) writeData(f, 'GrowthAmplitude', self.__growthAmplitude, 1) writeClos(f, 'Environment', 0)
def writeSensors(self, f:
) -
Write voxel sensors to a text file using the .vxa format.
Args
f
- File to write to
Returns
None
Expand source code
def writeSensors(self, f: TextIO): """ Write voxel sensors to a text file using the .vxa format. Args: f: File to write to Returns: None """ writeOpen(f, 'Sensors', 0) for sensor in self.__sensors: writeOpen(f, 'Sensor', 1) if sensor.name is not None: writeData(f, 'Name', sensor.name, 2) writeData(f, 'Location', str(sensor.coords).replace('(', '').replace(',', '').replace(')', ''), 2) writeData(f, 'Axis', sensor.axis.value, 2) writeClos(f, 'Sensor', 1) writeClos(f, 'Sensors', 0)
def writeSimData(self, f:
) -
Write simulation parameters to a text file using the .vxa format.
Args
f
- File to write to
Returns
None
Expand source code
def writeSimData(self, f: TextIO): """ Write simulation parameters to a text file using the .vxa format. Args: f: File to write to Returns: None """ # Simulator settings writeOpen(f, 'Simulator', 0) writeOpen(f, 'Integration', 1) writeData(f, 'Integrator', self.__integrator, 2) writeData(f, 'DtFrac', self.__dtFraction, 2) writeClos(f, 'Integration', 1) writeOpen(f, 'Damping', 1) writeData(f, 'BondDampingZ', self.__dampingBond, 2) writeData(f, 'ColDampingZ', self.__collisionDamping, 2) writeData(f, 'SlowDampingZ', self.__dampingEnvironment, 2) writeClos(f, 'Damping', 1) writeOpen(f, 'Collisions', 1) writeData(f, 'SelfColEnabled', int(self.__collisionEnable), 2) writeData(f, 'ColSystem', self.__collisionSystem, 2) writeData(f, 'CollisionHorizon', self.__collisionHorizon, 2) writeClos(f, 'Collisions', 1) writeOpen(f, 'Features', 1) writeData(f, 'BlendingEnabled', int(self.__blendingEnable), 2) writeData(f, 'XMixRadius', self.__xMixRadius, 2) writeData(f, 'YMixRadius', self.__yMixRadius, 2) writeData(f, 'ZMixRadius', self.__zMixRadius, 2) writeData(f, 'BlendModel', self.__blendingModel, 2) writeData(f, 'PolyExp', self.__polyExp, 2) writeData(f, 'VolumeEffectsEnabled', int(self.__volumeEffectsEnable), 2) writeClos(f, 'Features', 1) writeOpen(f, 'StopCondition', 1) writeData(f, 'StopConditionType', self.__stopConditionType.value, 2) writeData(f, 'StopConditionValue', self.__stopConditionValue, 2) writeClos(f, 'StopCondition', 1) writeOpen(f, 'EquilibriumMode', 1) writeData(f, 'EquilibriumModeEnabled', int(self.__equilibriumModeEnable), 2) writeClos(f, 'EquilibriumMode', 1) writeClos(f, 'Simulator', 0)
def writeTempControls(self, f:
) -
Write temperature control element to a text file using the .vxa format.
Args
f
- File to write to
Returns
None
Expand source code
def writeTempControls(self, f: TextIO): """ Write temperature control element to a text file using the .vxa format. Args: f: File to write to Returns: None """ writeData(f, 'EnableHydrogelModel', int(self.__hydrogelModelEnable), 0) writeOpen(f, 'TempControls', 0) for group in self.__localTempControls: writeOpen(f, 'Element', 1) if group.name is not None: writeData(f, 'Name', group.name, 2) writeOpen(f, 'Locations', 2) for loc in group.locations: writeData(f, 'Location', str(loc).replace('(', '').replace(',', '').replace(')', ''), 3) writeClos(f, 'Locations', 2) writeOpen(f, 'Keyframes', 2) for keyframe in group.keyframes: writeOpen(f, 'Keyframe', 3) writeData(f, 'TimeValue', keyframe.time_value, 4) writeData(f, 'AmplitudePos', keyframe.amplitude_pos, 4) writeData(f, 'AmplitudeNeg', keyframe.amplitude_neg, 4) writeData(f, 'PercentPos', keyframe.percent_pos, 4) writeData(f, 'Period', keyframe.period, 4) writeData(f, 'PhaseOffset', keyframe.phase_offset, 4) writeData(f, 'TempOffset', keyframe.temp_offset, 4) writeData(f, 'ConstantTemp', int(keyframe.const_temp), 4) writeData(f, 'SquareWave', int(keyframe.square_wave), 4) writeClos(f, 'Keyframe', 3) writeClos(f, 'Keyframes', 2) writeClos(f, 'Element', 1) writeClos(f, 'TempControls', 0)
class StopCondition (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Options for simulation stop conditions.
Expand source code
class StopCondition(Enum): """ Options for simulation stop conditions. """ NONE = 0 TIME_STEP = 1 TIME_VALUE = 2 TEMP_CYCLES = 3 ENERGY_CONST = 4 ENERGY_KFLOOR = 5 MOTION_FLOOR = 6
Ancestors
- enum.Enum
Class variables
var ENERGY_CONST
var ENERGY_KFLOOR
var MOTION_FLOOR
var NONE
var TEMP_CYCLES
var TIME_STEP
var TIME_VALUE