#!/usr/bin/python3 """ Author : "Ammar Qammaz" Copyright : "2022 Foundation of Research and Technology, Computer Science Department Greece, See license.txt" License : "FORTH" """ import os import sys import gc import time import json import tensorflow as tf from tensorflow import keras #from tensorflow.keras.backend.tensorflow_backend import set_session #from tensorflow.keras import backend as K from tensorflow.keras.layers import Input, Dense from tensorflow.keras.models import Model from tensorflow.keras.models import Sequential from tensorflow.keras.models import model_from_json from tensorflow.keras.utils import plot_model import tensorflow.keras.callbacks import numpy as np from numba import njit #Test from tools import bcolors,checkIfFileExists def printTrainingVersion(): print(""" ███╗ ███╗ ██████╗ ██████╗ █████╗ ██████╗ ███╗ ██╗███████╗████████╗ ████╗ ████║██╔═══██╗██╔════╝██╔══██╗██╔══██╗████╗ ██║██╔════╝╚══██╔══╝ ██╔████╔██║██║ ██║██║ ███████║██████╔╝██╔██╗ ██║█████╗ ██║ ██║╚██╔╝██║██║ ██║██║ ██╔══██║██╔═══╝ ██║╚██╗██║██╔══╝ ██║ ██║ ╚═╝ ██║╚██████╔╝╚██████╗██║ ██║██║ ██║ ╚████║███████╗ ██║ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝╚══════╝ ╚═╝ """) #----------------------------- #BMVC 21 paper was submitted with Keras 2.2.4/Tensorflow 1.14/Numpy 1.16.2 #----------------------------- print("") print("Tensorflow version : ",tf.__version__) #print("Keras version : ",keras.__version__) <- no longer available in TF-2.13 print("Numpy version : ",np.__version__) #----------------------------- from tensorflow.python.platform import build_info as tf_build_info print("TF/CUDA version : ",tf_build_info.build_info['cuda_version']) print("TF/CUDNN version : ",tf_build_info.build_info['cudnn_version']) #----------------------------- print("") #----------------------------- def forceCPU(): print(bcolors.WARNING,"User selected to use CPU mode",bcolors.ENDC) os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" # see issue #152 os.environ["CUDA_VISIBLE_DEVICES"] = "" def limitGPUMemory(): print("Limiting GPU memory usage for multiple instances..\n") config = tf.compat.v1.ConfigProto() config.gpu_options.per_process_gpu_memory_fraction = 0.2 config.gpu_options.visible_device_list = "0" set_session(tf.compat.v1.Session(config=config)) def setTensorflowBackendToHalfFloats(): dtype='float16' #WARNING:tensorflow:Mixed precision compatibility check (mixed_float16): WARNING #Your GPU may run slowly with dtype policy mixed_float16 because it does not have compute capability #of at least 7.0. Your GPU: Quadro P6000, compute capability 6.1 from tensorflow.keras.mixed_precision import experimental as mixed_precision policy = mixed_precision.Policy('mixed_float16') mixed_precision.set_policy(policy) tf.keras.backend.set_floatx(dtype) # default is 1e-7 which is too small for float16. Without adjusting the epsilon, we will get NaN predictions because of divide by zero problems tf.keras.backend.set_epsilon(1e-4) print('Compute dtype: %s' % policy.compute_dtype) print('Variable dtype: %s' % policy.variable_dtype) print("\nUsing Half-Floats for training ") def getRSquared(groundTruth, neuralNetworkOutput): """ Computes the R-squared metric. https://en.wikipedia.org/wiki/Coefficient_of_determination Parameters: -- groundTruth (numpy.ndarray): 1D array representing the ground truth. -- neuralNetworkOutput (numpy.ndarray): 1D array representing the neural network output. Returns: -- float: R-squared metric. """ # Compute the mean of the ground truth mean_ground_truth = np.mean(groundTruth) # Compute the sum of squares of the residuals ss_residuals = np.sum((groundTruth - neuralNetworkOutput) ** 2) # Compute the sum of squares of the total variation ss_total = np.sum((groundTruth - mean_ground_truth) ** 2) # Compute the R-squared metric r_squared = 1 - (ss_residuals / ss_total) return r_squared @njit def getRSquaredInPlace(groundTruth,mean_ground_truth,N,neuralNetworkOutput): """ Computes the R-squared metric. https://en.wikipedia.org/wiki/Coefficient_of_determination Parameters: -- groundTruth (numpy.ndarray): 1D array representing the ground truth. -- neuralNetworkOutput (numpy.ndarray): 1D array representing the neural network output. Returns: -- float: R-squared metric. """ #N = groundTruth.shape[0] if N == 0: return np.NAN # Compute the mean of the ground truth #mean_ground_truth = np.mean(groundTruth) ss_residuals = np.float32(0.0) ss_total = np.float32(0.0) for i in range(N): #-------------------------------------- gt = np.float32(groundTruth[i][0]) #groundTruth is a tensorflow array so each element is also an array -> thus we need [0] nn_out = np.float32(neuralNetworkOutput[i][0]) #neuralNetworkOutput is a tensorflow array so each element is also an array -> thus we need [0] #-------------------------------------- delta = gt - nn_out ss_residuals += delta ** 2 #-------------------------------------- delta = gt - mean_ground_truth ss_total += delta ** 2 # Compute the R-squared metric r_squared = np.float32(1 - (ss_residuals / ss_total)) return r_squared def getRSquaredForNeuralNetwork(mocapNETNetwork,networkInput,groundtruthOutput): import time startAt = time.time() #----------------------------------------------------- try: print("Extracting R² : Getting neural network response..") predictions = mocapNETNetwork.predict(networkInput) #Since the groundtruth is very large and the machines I use don't have #enough RAM this is an in place R^2 calculation to make sure the computer doesn't enter a page swapping #loop that makes training grind to a halt..! print("Extracting R² : Calculating..") mean_ground_truth = np.float32(np.mean(groundtruthOutput)) #print("groundtruthOutput: ",groundtruthOutput) rSQ = getRSquaredInPlace(groundtruthOutput,mean_ground_truth,groundtruthOutput.shape[0],predictions) #This is faster (if you have spare memory..) #rSQ = getRSquared(groundtruthOutput,predictions) del predictions #except: except Exception as e: print("Failed to extract R² ..") print("Error was : ",e) rSQ = np.NAN #----------------------------------------------------- #----------------------------------------------------- endAt = time.time() print("R² = ",rSQ," it took ",(endAt-startAt)/60," mins to compute") return rSQ def logTrainingResults( filename, configuration, outputType, outputName, history, metrics ): if (outputType==0): numberOfModelParametersStr="?" if 'modelParameters' in configuration: numberOfModelParametersStr="%u" % int(configuration['modelParameters']) #First record - Start of File inputProcessing="" if (configuration['eNSRM']): inputProcessing=inputProcessing+" eNSRM" if ('NSRMNormalizeAngles' in configuration) and (configuration['NSRMNormalizeAngles']): if (configuration['NSRMNormalizeAngles']==1): inputProcessing=inputProcessing+" NSRMNormalizeAngles" #Half elif (configuration['NSRMNormalizeAngles']==2): inputProcessing=inputProcessing+" NSRMNormalizeAnglesFull" else: inputProcessing=inputProcessing+" NSRMNormalizeAnglesUNKNOWN" if (configuration['EDM']): inputProcessing=inputProcessing+" EDM" if (configuration['eigenPoses']): inputProcessing=inputProcessing+" eigenPoses" if (configuration['autobuilder']): inputProcessing=inputProcessing+" autobuilder" if ( (configuration['decompositionType']!="") and (configuration['doPCA']!="") and ( int(configuration['PCADimensionsKept'])>0 ) ): inputProcessing=inputProcessing+" %s(%u)"%(configuration['decompositionType'],configuration['PCADimensionsKept']) if (configuration['PCAAlsoKeepRawData']): inputProcessing=inputProcessing+"+PCA_RawData" inputProcessing=inputProcessing+" act("+configuration['activationFunction']+")" inputProcessing=inputProcessing+" rand("+configuration['weightRandomizationFunction']+")" inputProcessing=inputProcessing+" "+configuration['optimizer'] if (configuration['balanced2DInputs']): inputProcessing=inputProcessing+" balanced2DInputs" if (configuration['useQuadLoss']!=0): inputProcessing=inputProcessing+" quadLoss" if (configuration['setConstantSeedForReproducibleTraining']!=1): inputProcessing=inputProcessing+" random seed" if (configuration['include2DInputVisibilityFlags']==0): inputProcessing=inputProcessing+" novis2Dflag" experimentDescription="" experimentDescription=experimentDescription+"group %u"%(configuration["groupOutputs"]) if (configuration['useRadians']): experimentDescription=experimentDescription+" radians" #-------------------------------------------------------------------- if ('outputValueDistribution' in configuration): if (configuration['outputValueDistribution']=='balanced'): experimentDescription=experimentDescription+" balanced offset" elif (configuration['outputValueDistribution']=='positive'): experimentDescription=experimentDescription+" positive offset" elif (configuration['outputValueDistribution']=='negative'): experimentDescription=experimentDescription+" negative offset" #-------------------------------------------------------------------- if ('outputNormalizationStrategy' in configuration): experimentDescription=experimentDescription+" outputNormalizationStrategy="+configuration['outputNormalizationStrategy'] #-------------------------------------------------------------------- if (float(configuration['outputMultiplier'])!=1.0): experimentDescription=experimentDescription+" outputScaling=%0.2f" % float(configuration['outputMultiplier']) experimentDescription=experimentDescription+" NNDepth=%u"%configuration['neuralNetworkDepth'] #FIRST! RECORD! with open(filename, 'w') as f: f.write(""" Training summary of experiment %s for %s """%(configuration['label'],configuration['hierarchyPartName'])) #Add the hostname.. import socket f.write("%s
\n"%socket.gethostname()) f.write("""
Date Part BatchSize Epochs λ Remember Occlusions Input Processing Hard Mining
%s %s %u %u/%u/%u %0.2f c %u/p %u %u %s
"""% ( configuration['date'], configuration['hierarchyPartName'], configuration['defaultBatchSize'], configuration["defaultNumberOfEpochs"],configuration["highNumberOfEpochs"],configuration["veryHighNumberOfEpochs"], configuration["lamda"], configuration["rememberConsecutiveWeights"],configuration["rememberWeights"], configuration["ignoreOcclusions"]==0, inputProcessing ) ) f.write("""
Serial Start End Training Samples Model Size Dropout/L.R Description
%s %u %u %u %s %0.2f/%0.5f %s
"""% ( configuration['label'], configuration['startPosition'], configuration['endPosition'], configuration['trainingSamples'], numberOfModelParametersStr, configuration['dropoutRate'], configuration['learningRate'], experimentDescription ) ) f.close() #---------------------------------- try: lowestLossAchievedAt = 0 if ("loss" in history.history) and ("mae" in history.history): # Initialize variables to store the initial and lowest loss initial_loss = history.history['loss'][0] lowest_loss = initial_loss initial_mae = history.history['mae'][0] lowest_mae = initial_mae # Iterate through the history object count = 0 for i, loss in enumerate(history.history['loss']): # Update the lowest loss if a lower value is found if loss < lowest_loss: lowest_loss = loss lowestLossAchievedAt = count count = count + 1 lowest_mae = history.history['mae'][lowestLossAchievedAt] #By default don't scale #---------------------------------------------------------------------------- outputMinimumValue = 0.0 outputMaximumValue = 0.0 outputOffsetValue = 0.0 outputScalarValues = 1.0 outputScalarFractionValues = 1.0 #---------------------------------------------------------------------------- for v in range(0,len(configuration["outputOffsetLabels"])): if (outputName.lower()==configuration["outputOffsetLabels"][v].lower()): #we found a rule! outputMinimumValue = float(configuration["outputOffsetMinima"][v]) outputMaximumValue = float(configuration["outputOffsetMaxima"][v]) outputOffsetValue = float(configuration["outputOffsetValues"][v]) outputScalarValues = float(configuration["outputScalarValues"][v]) outputScalarFractionValues = float(configuration["outputScalarFractionValues"][v]) #---------------------------------------------------------------------------- initialVAL = "" lowestVAL = "" if ("val_loss" in history.history) and ("val_mae" in history.history): initial_VALloss = history.history['val_loss'][0] lowest_VALloss = history.history['val_loss'][lowestLossAchievedAt] initial_VALmae = history.history['val_mae'][0] lowest_VALmae = history.history['val_mae'][lowestLossAchievedAt] initial_VALmae = initial_VALmae * outputScalarFractionValues lowest_VALmae = lowest_VALmae * outputScalarFractionValues #-------------------------------------- initialVAL = " - " if ("test_rsquared_start" in metrics): initialVAL = " - R² %0.2f/"%(metrics["test_rsquared_start"]) initialVAL = initialVAL + "%0.4f/%0.4f" % (initial_VALloss,initial_VALmae) #-------------------------------------- lowestVAL = " - " if ("test_rsquared_end" in metrics): lowestVAL = " - R² %0.2f/"%(metrics["test_rsquared_end"]) lowestVAL = lowestVAL + "%0.4f/%0.4f" % (lowest_VALloss,lowest_VALmae) #Every output now gets scaled always initial_mae = initial_mae * outputScalarFractionValues lowest_mae = lowest_mae * outputScalarFractionValues with open(filename, 'a') as f: f.write("") #------------------------------------------------ #Start Loss f.write("") #------------------------------------------------ #End Loss f.write("") #------------------------------------------------ #Training Epochs ---------------------------- f.write("") #Min/Max ---------------------------- f.write("") #Offset ----------------------------- f.write("") #Scalar ----------------------------- f.write("") f.write("") #------------------------------------------------ f.write("") f.close() except: print("An exception occurred in logTrainingResults") def regularTraining(tensorboardLabel,mocapNETNetwork,numberOfEpochs,batchSize,earlyStoppingPatience,trainInput,trainOutput,testInput,testOutput,minD,modelIsTrivial=False,haveTestSet=False,useHalfFloats=False): #We use a checkpoint system to return best model.. if (os.path.isfile("bestW.hdf5")): print(bcolors.WARNING,"Found a forgotten checkpoint file, erasing it to avoid trouble",bcolors.ENDC) os.system('rm bestW.hdf5') #whatToMonitor='mean_absolute_error' #minimumDelta=0.05 whatToMonitor='loss' metrics = dict() if (useHalfFloats): print("Early stopping will use MAE instead of loss due to half floats..") whatToMonitor='mae' minimumDelta=minD #To see use : #tensorboard --logdir step0_upperbody_all/tensorboard --bind_all #------------------------------------------------------------------------ tensorboard = keras.callbacks.TensorBoard(log_dir=tensorboardLabel,histogram_freq=1) #------------------------------------------------------------------------ earlystopper = keras.callbacks.EarlyStopping( monitor=whatToMonitor, min_delta=minimumDelta, patience=earlyStoppingPatience, verbose=1, mode='auto' ) #------------------------------------------------------------------------- checkpointer = keras.callbacks.ModelCheckpoint( filepath="bestW.hdf5", monitor=whatToMonitor, mode='min', verbose=1, save_freq='epoch', save_best_only=True, save_weights_only=True ) #------------------------------------------------------------------------ callbacks = [ earlystopper, checkpointer, tensorboard ] #------------------------------------------------------------------------ print("regularTraining begins -> BatchSize=%u / NumberOfEpochs=%u "%(batchSize,numberOfEpochs)) #------------------------------------------------------------------------ if (haveTestSet==False): #Train without any test data ( probably to conserve memory ) if (not modelIsTrivial): metrics["train_rsquared_start"] = getRSquaredForNeuralNetwork(mocapNETNetwork,trainInput,trainOutput) history = mocapNETNetwork.fit( trainInput,trainOutput, epochs=numberOfEpochs, batch_size=batchSize, shuffle=True, #steps_per_epoch=5, #<- Use more steps per epoch? callbacks=callbacks ) if (not modelIsTrivial): metrics["train_rsquared_end"] = getRSquaredForNeuralNetwork(mocapNETNetwork,trainInput,trainOutput) else: #Train with test data if (not modelIsTrivial): metrics["train_rsquared_start"] = getRSquaredForNeuralNetwork(mocapNETNetwork,trainInput,trainOutput) metrics["test_rsquared_start"] = getRSquaredForNeuralNetwork(mocapNETNetwork,testInput,testOutput) history = mocapNETNetwork.fit( trainInput,trainOutput, epochs=numberOfEpochs, batch_size=batchSize, shuffle=True, validation_data=(testInput,testOutput), #validation_split=0.2, callbacks=callbacks ) if (not modelIsTrivial): metrics["train_rsquared_end"] = getRSquaredForNeuralNetwork(mocapNETNetwork,trainInput,trainOutput) metrics["test_rsquared_end"] = getRSquaredForNeuralNetwork(mocapNETNetwork,testInput,testOutput) #------------------------------------------------------------------------ if (checkIfFileExists('bestW.hdf5')): print("We use the best possible model from our ModelCheckpoint") mocapNETNetwork.load_weights('bestW.hdf5') os.system('rm bestW.hdf5') #Get rid of the checkpoint as soon as we are done reading it else: print("No bestW.hdf5 file found, just using last iteration..") #------------------------------------------------------------------------ return history,mocapNETNetwork,metrics #------------------------------------------------------------------------------------------------------------------------------------------------- def compareTrainingOutputs(groundTruth,neuralOutput): if (len(groundTruth)!=len(neuralOutput)): print("Outputs have a different size") return np.nan values = len(groundTruth) losses = (groundTruth[:]-neuralOutput[:])**2 return losses #---------------------------------------------------- def appendHistory(baseHistory,historyToAppend): #print("baseHistory -> ",type(baseHistory)) #print("baseHistory Contents -> ",baseHistory) #print("historyToAppend -> ",type(historyToAppend)) #print("historyToAppend Contents -> ",historyToAppend) #print("baseHistory.history -> ",type(baseHistory.history)) #print("historyToAppend.history -> ",type(historyToAppend.history)) #----------------------------------------------------------------------------------------------------------- if ("mae" in baseHistory.history) and ("mae" in historyToAppend.history): baseHistory.history['mae'] = baseHistory.history['mae'] + historyToAppend.history['mae'] if ("loss" in baseHistory.history) and ("loss" in historyToAppend.history): baseHistory.history['loss'] = baseHistory.history['loss'] + historyToAppend.history['loss'] if ("val_loss" in baseHistory.history) and ("val_loss" in historyToAppend.history): baseHistory.history['val_loss'] = baseHistory.history['val_loss'] + historyToAppend.history['val_loss'] if ("val_mae" in baseHistory.history) and ("val_mae" in historyToAppend.history): baseHistory.history['val_mae'] = baseHistory.history['val_mae'] + historyToAppend.history['val_mae'] #----------------------------------------------------------------------------------------------------------- return baseHistory def getLossManually(mocapNETNetwork,trainInput,trainOutput): predictions = mocapNETNetwork.predict(trainInput) losses = compareTrainingOutputs(predictions,trainOutput) #print(losses) print("Calculating Loss Statistics :") #-------------------------- maximum = np.max(losses) minimum = np.min(losses) median = np.median(losses) mean = np.mean(losses) from tools import calculateStandardDeviationInPlaceKnowingMean,convertStandardDeviationToVariance std = calculateStandardDeviationInPlaceKnowingMean(losses,mean) #np.std(losses) var = convertStandardDeviationToVariance(std) #np.var(losses) #-------------------------- titleString="Min=%0.2f,Max=%0.2f,Median=%0.2f,Mean=%0.2f,Std=%0.2f,Var=%0.2f" % (minimum,maximum,median,mean,std,var) print(bcolors.WARNING," %s " % titleString,bcolors.ENDC) #difficultyRating = 1.5 difficultyRating = 5.0 # 4.0 1.5 2.8 badScoreAbove = mean + difficultyRating * np.sqrt(std) print("Threshold for difficult poses is ",badScoreAbove) difficultPosesIndexes=list() numberOfTrainingSamples = len(trainInput) for z in range(0,numberOfTrainingSamples): if (np.any(losses[z]>badScoreAbove)): difficultPosesIndexes.append(z) del losses return difficultPosesIndexes,mean,std def onlineHardExampleMiningTraining(tensorboardLabel,mocapNETNetwork,numberOfEpochs,numberOfHardEpochs,numberOfNormalEpochsAfterHard,batchSize,earlyStoppingPatience,trainInput,trainOutput,testInput,testOutput,minD,modelIsTrivial=False,haveTestSet=False,useHalfFloats=False): #First do a regular training.. halfEpochs=int(numberOfEpochs/2) if (numberOfEpochs==1): halfEpochs=numberOfEpochs print(" Online Hard Example Mining Training session .. ") metrics = dict() if (not modelIsTrivial): metrics["train_rsquared_start"] = getRSquaredForNeuralNetwork(mocapNETNetwork,trainInput,trainOutput) dtypeSelected=np.float32 if (useHalfFloats): dtypeSelected=np.float16 totalHistory,mocapNETNetwork = regularTraining(tensorboardLabel,mocapNETNetwork,halfEpochs,batchSize,earlyStoppingPatience,trainInput,trainOutput,testInput,testOutput,minD,haveTestSet=haveTestSet,useHalfFloats=useHalfFloats) if (numberOfEpochs==1): print(" Trivial output will not mine etc. ") return totalHistory bestTrainingMAE=0.0 successfulUpdates=0 REGULAR_EPOCHS_AFTER_HARD=numberOfNormalEpochsAfterHard HARD_EPOCHS=numberOfHardEpochs miningEpochs = int(halfEpochs/2) print("Getting initial state of network") difficultPosesIndexes,mean,std = getLossManually(mocapNETNetwork,trainInput,trainOutput) print("Backing up model with mae ",mean) mocapNETNetwork.save_weights("modelBackup.h5") bestTrainingMAE=mean for i in range(0,miningEpochs): #------------------------------------------------------------------------------------------------------------------------------------------------------- #------------------------------------------------------------------------------------------------------------------------------------------------------- #------------------------------------------------------------------------------------------------------------------------------------------------------- numberOfTrainingSamples = len(trainInput) inputSize = len (trainInput[1]) outputSize = len (trainOutput[1]) ratioOfDatasetThatIsDifficult = float ( len(difficultPosesIndexes) / numberOfTrainingSamples ) #If more than 10% of the dataset is difficult if (ratioOfDatasetThatIsDifficult>0.5): print(bcolors.FAIL," More than half of the dataset is hard (%u samples) so we train on all samples as hard %u/%u" % (len(difficultPosesIndexes),i,miningEpochs),bcolors.ENDC) regularHistory,mocapNETNetwork = regularTraining(tensorboardLabel,mocapNETNetwork,HARD_EPOCHS,batchSize,5,trainInput,trainOutput,testInput,testOutput,minD,haveTestSet=haveTestSet,useHalfFloats=useHalfFloats) #difficultInput,difficultOutput totalHistory = appendHistory(totalHistory,regularHistory) elif (ratioOfDatasetThatIsDifficult>0.1): difficultInput = np.full([len(difficultPosesIndexes),inputSize],fill_value=0,dtype=dtypeSelected,order='C') difficultOutput = np.full([len(difficultPosesIndexes),outputSize],fill_value=0,dtype=dtypeSelected,order='C') for z in range(0,len(difficultPosesIndexes)): for field in range(0,inputSize): difficultInput[z,field]=trainInput[difficultPosesIndexes[z],field] for field in range(0,outputSize): difficultOutput[z,field]=trainOutput[difficultPosesIndexes[z],field] print(bcolors.OKBLUE," Will now train on %u difficult samples %u/%u" % (len(difficultPosesIndexes),i,miningEpochs),bcolors.ENDC) newHistory,mocapNETNetwork = regularTraining(tensorboardLabel,mocapNETNetwork,HARD_EPOCHS,batchSize,5,difficultInput,difficultOutput,testInput,testOutput,minD,haveTestSet=haveTestSet,useHalfFloats=useHalfFloats) totalHistory = appendHistory(totalHistory,newHistory) del difficultInput del difficultOutput if (mean
Joint R² / Start Loss / M.A.E R² / End Loss / M.A.E Tr.Epochs Min/Max Offset Scalar Scalar Fr.
") f.write(outputName) f.write("") if ("train_rsquared_start" in metrics): f.write("R² %0.2f/"%(metrics["train_rsquared_start"])) f.write("%0.4f/%0.4f%s"%(initial_loss,initial_mae,initialVAL)) f.write("") if ("train_rsquared_end" in metrics): f.write("R² %0.2f/"%(metrics["train_rsquared_end"])) f.write("%0.4f/%0.4f%s"%(lowest_loss,lowest_mae,lowestVAL)) f.write("") f.write("%u"%(lowestLossAchievedAt)) f.write("") f.write("%0.4f/%0.4f"%(outputMinimumValue,outputMaximumValue)) f.write("") f.write("%0.4f"%(outputOffsetValue)) f.write("") f.write("%0.4f"%(outputScalarValues)) f.write("") f.write("%0.4f"%(outputScalarFractionValues)) f.write("