diff --git a/ApplicationCode/GrpcInterface/CMakeLists.cmake b/ApplicationCode/GrpcInterface/CMakeLists.cmake index 89d6ab410a..1e6000dcb6 100644 --- a/ApplicationCode/GrpcInterface/CMakeLists.cmake +++ b/ApplicationCode/GrpcInterface/CMakeLists.cmake @@ -173,6 +173,7 @@ if (RESINSIGHT_GRPC_PYTHON_EXECUTABLE) "rips/PythonExamples/CommandExample.py" "rips/PythonExamples/CaseGridGroup.py" "rips/PythonExamples/CaseInfoStreamingExample.py" + "rips/PythonExamples/ExportSnapshots.py" "rips/PythonExamples/ErrorHandling.py" "rips/PythonExamples/SoilPorvAsync.py" "rips/PythonExamples/SoilPorvSync.py" @@ -186,7 +187,7 @@ if (RESINSIGHT_GRPC_PYTHON_EXECUTABLE) "rips/PythonExamples/InputPropTestAsync.py" "rips/PythonExamples/SoilAverageAsync.py" "rips/PythonExamples/SoilAverageSync.py" - "rips/PythonExamples/SoilAverageNoComm.py" + "rips/PythonExamples/ViewExample.py" "rips/tests/test_cases.py" "rips/tests/test_commands.py" "rips/tests/test_grids.py" diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/AllCases.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/AllCases.py index 1064eb8545..f54195c1ff 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/AllCases.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/AllCases.py @@ -1,14 +1,18 @@ +################################################################################### +# This example will connect to ResInsight, retrieve a list of cases and print info +################################################################################### + +# Import the ResInsight Processing Server Module import rips +# Connect to ResInsight resInsight = rips.Instance.find() if resInsight is not None: + # Get a list of all cases cases = resInsight.project.cases() print ("Got " + str(len(cases)) + " cases: ") for case in cases: - print(case.name) - views = case.views() - for view in views: - view.setShowGridBox(not view.showGridBox()) - view.setBackgroundColor("#3388AA") - view.update() + print("Case name: " + case.name) + print("Case grid path: " + case.gridPath()) + diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/CaseInfoStreamingExample.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/CaseInfoStreamingExample.py index b0457173ea..6f81aa0693 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/CaseInfoStreamingExample.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/CaseInfoStreamingExample.py @@ -1,22 +1,27 @@ +############################################################################### +# This example will get the cell info for the active cells for the first case +############################################################################### + +# Import the ResInsight Processing Server Module import rips +# Connect to ResInsight resInsight = rips.Instance.find() +# Get the case with id == 0. This will fail if your project doesn't have a case with id == 0 case = resInsight.project.case(id = 0) +# Get the cell count object cellCounts = case.cellCount() print("Number of active cells: " + str(cellCounts.active_cell_count)) +print("Total number of reservoir cells: " + str(cellCounts.reservoir_cell_count)) -activeCellInfoChunks = case.cellInfoForActiveCells() +# Get information for all active cells +activeCellInfos = case.cellInfoForActiveCells() -#print("Number of grids: " + str(gridCount)) -#print(gridDimensions) +# A simple check on the size of the cell info +assert(cellCounts.active_cell_count == len(activeCellInfos)) -receivedActiveCells = [] -for activeCellChunk in activeCellInfoChunks: - for activeCell in activeCellChunk.data: - receivedActiveCells.append(activeCell) - -assert(cellCounts.active_cell_count == len(receivedActiveCells)) +# Print information for the first active cell print("First active cell: ") -print(receivedActiveCells[0]) +print(activeCellInfos[0]) diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/CommandExample.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/CommandExample.py index 26f364eea3..a32760a367 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/CommandExample.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/CommandExample.py @@ -1,3 +1,8 @@ +############################################################################### +# This example will run a few ResInsight command file commands +# .. which are exposed in the Python interface. +# Including setting time step, window size and export snapshots and properties +############################################################################### import os import tempfile import rips @@ -8,16 +13,29 @@ resInsight = rips.Instance.find() # Run a couple of commands resInsight.commands.setTimeStep(caseId=0, timeStep=3) resInsight.commands.setMainWindowSize(width=800, height=500) -#resInsight.commands.exportWellPaths() + +# Create a temporary directory which will disappear at the end of this script +# If you want to keep the files, provide a good path name instead of tmpdirname with tempfile.TemporaryDirectory(prefix="rips") as tmpdirname: print("Temporary folder: ", tmpdirname) + + # Set export folder for snapshots and properties resInsight.commands.setExportFolder(type='SNAPSHOTS', path=tmpdirname) resInsight.commands.setExportFolder(type='PROPERTIES', path=tmpdirname) + + # Export snapshots resInsight.commands.exportSnapshots() + + # Print contents of temporary folder print(os.listdir(tmpdirname)) + assert(len(os.listdir(tmpdirname)) > 0) case = resInsight.project.case(id=0) + + # Export properties in the view resInsight.commands.exportPropertyInViews(0, "3D View", 0) + + # Check that the exported file exists expectedFileName = case.name + "-" + str("3D_View") + "-" + "T3" + "-SOIL" fullPath = tmpdirname + "/" + expectedFileName assert(os.path.exists(fullPath)) diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ErrorHandling.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ErrorHandling.py index dacccaed71..d36d99f923 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ErrorHandling.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ErrorHandling.py @@ -1,3 +1,8 @@ +################################################################### +# This example demonstrates the use of ResInsight exceptions +# for proper error handling +################################################################### + import rips import grpc diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ExportSnapshots.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ExportSnapshots.py new file mode 100644 index 0000000000..8a5ef4cbc7 --- /dev/null +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ExportSnapshots.py @@ -0,0 +1,43 @@ +############################################################################ +# This script will export snapshots for two properties in every loaded case +# And put them in a snapshots folder in the same folder as the case grid +############################################################################ +import os +import rips + +# Load instance +resInsight = rips.Instance.find() +cases = resInsight.project.cases() + +# Set main window size +resInsight.commands.setMainWindowSize(width=800, height=500) + +n = 5 # every n-th timestep for snapshot +property_list = ['SOIL', 'PRESSURE'] # list of parameter for snapshot + +print ("Looping through cases") +for case in cases: + # Get grid path and its folder name + casepath = case.gridPath() + foldername = os.path.dirname(casepath) + + # create a folder to hold the snapshots + dirname = os.path.join(foldername, 'snapshots') + + if os.path.exists(dirname) is False: + os.mkdir(dirname) + + print ("Exporting to folder: " + dirname) + resInsight.commands.setExportFolder(type='SNAPSHOTS', path=dirname) + + timeSteps = case.timeSteps() + tss_snapshot = range(0, len(timeSteps), n) + print(case.name, case.id, 'Number of timesteps: ' + str(len(timeSteps))) + print('Number of timesteps for snapshoting: ' + str(len(tss_snapshot))) + + view = case.view(id = 0) + for property in property_list: + view.applyCellResult(resultType='DYNAMIC_NATIVE', resultVariable=property) + for ts_snapshot in tss_snapshot: + resInsight.commands.setTimeStep(caseId = case.id, timeStep = ts_snapshot) + resInsight.commands.exportSnapshots('VIEWS') # ‘ALL’, ‘VIEWS’ or ‘PLOTS’ default is 'ALL' diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/GridInformation.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/GridInformation.py index 410a261efa..52ec8c3fca 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/GridInformation.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/GridInformation.py @@ -1,6 +1,11 @@ +###################################################################################### +# This example prints information about the grids of all cases in the current project +###################################################################################### + import rips resInsight = rips.Instance.find() + cases = resInsight.project.cases() print("Number of cases found: ", len(cases)) for case in cases: diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InputPropTestAsync.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InputPropTestAsync.py index afb56bc588..2db56bbf81 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InputPropTestAsync.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InputPropTestAsync.py @@ -1,24 +1,38 @@ +######################################################################################## +# This example generates a derived property in an asynchronous manner +# Meaning it does not wait for all the data for each stage to be read before proceeding +######################################################################################## import rips import time +# Internal function for creating a result from a small chunk of poro and permx results +# The return value of the function is a generator for the results rather than the result itself. def createResult(poroChunks, permxChunks): + # Loop through all the chunks of poro and permx in order for (poroChunk, permxChunk) in zip(poroChunks, permxChunks): resultChunk = [] + # Loop through all the values inside the chunks, in order for (poro, permx) in zip(poroChunk.values, permxChunk.values): resultChunk.append(poro * permx) + # Return a generator object that behaves like a Python iterator yield resultChunk resInsight = rips.Instance.find() start = time.time() case = resInsight.project.case(id=0) +# Get a generator for the poro results. The generator will provide a chunk each time it is iterated poroChunks = case.properties.activeCellPropertyAsync('STATIC_NATIVE', 'PORO', 0) +# Get a generator for the permx results. The generator will provide a chunk each time it is iterated permxChunks = case.properties.activeCellPropertyAsync('STATIC_NATIVE', 'PERMX', 0) +# Send back the result with the result provided by a generator object. +# Iterating the result generator will cause the script to read from the poro and permx generators +# And return the result of each iteration case.properties.setActiveCellPropertyAsync(createResult(poroChunks, permxChunks), 'GENERATED', 'POROPERMXAS', 0) end = time.time() print("Time elapsed: ", end - start) - -print("Transferred all results back") \ No newline at end of file +print("Transferred all results back") +view = case.views()[0].applyCellResult('GENERATED', 'POROPERMXAS') \ No newline at end of file diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InputPropTestSync.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InputPropTestSync.py index 7354fc2ced..577880a1d6 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InputPropTestSync.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InputPropTestSync.py @@ -1,3 +1,8 @@ +######################################################################################## +# This example generates a derived property in an synchronous manner +# Meaning it completes reading each result before calculating the derived result +# See InputPropTestAsync for how to do this asynchronously instead. +######################################################################################## import rips import time import grpc @@ -6,14 +11,18 @@ resInsight = rips.Instance.find() start = time.time() case = resInsight.project.case(id=0) +# Read poro result into list poroResults = case.properties.activeCellProperty('STATIC_NATIVE', 'PORO', 0) +# Read permx result into list permxResults = case.properties.activeCellProperty('STATIC_NATIVE', 'PERMX', 0) +# Generate output result results = [] for (poro, permx) in zip(poroResults, permxResults): results.append(poro * permx) -try: +try: + # Send back output result case.properties.setActiveCellProperty(results, 'GENERATED', 'POROPERMXSY', 0) except grpc.RpcError as e: print("Exception Received: ", e) @@ -21,4 +30,6 @@ except grpc.RpcError as e: end = time.time() print("Time elapsed: ", end - start) -print("Transferred all results back") \ No newline at end of file +print("Transferred all results back") + +view = case.views()[0].applyCellResult('GENERATED', 'POROPERMXSY') \ No newline at end of file diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InstanceExample.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InstanceExample.py index fcc15a8679..bf72ff22b4 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InstanceExample.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/InstanceExample.py @@ -1,3 +1,6 @@ +####################################### +# This example connects to ResInsight +####################################### import rips resInsight = rips.Instance.find() diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SelectedCases.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SelectedCases.py index 38faffa089..80fc48d35b 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SelectedCases.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SelectedCases.py @@ -1,3 +1,10 @@ +############################################################################ +# This example returns the currently selected cases in ResInsight +# Because running this script in the GUI takes away the selection +# This script does not run successfully from within the ResInsight GUI +# And will need to be run from the command line separately from ResInsight +############################################################################ + import rips resInsight = rips.Instance.find() diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetCellResult.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetCellResult.py index de0b3e31b1..5eea3d07f9 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetCellResult.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetCellResult.py @@ -1,3 +1,6 @@ +###################################################################### +# This script applies a cell result to the first view in the project +###################################################################### import rips resInsight = rips.Instance.find() diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetFlowDiagnosticsResult.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetFlowDiagnosticsResult.py index 949dfb2358..9e3523f44e 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetFlowDiagnosticsResult.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetFlowDiagnosticsResult.py @@ -1,3 +1,7 @@ +###################################################################### +# This script applies a flow diagnostics cell result to the first view in the project +###################################################################### + # Load ResInsight Processing Server Client Library import rips # Connect to ResInsight instance diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetGridProperties.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetGridProperties.py index 1cdec8eb51..a7f8f7878b 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetGridProperties.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SetGridProperties.py @@ -1,3 +1,6 @@ +###################################################################### +# This script sets values for SOIL for all grid cells in the first case in the project +###################################################################### import rips resInsight = rips.Instance.find() diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageAsync.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageAsync.py index 126607dec3..b6eccc186d 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageAsync.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageAsync.py @@ -1,3 +1,7 @@ +########################################################################################### +# This example will asynchronously calculate the average value for SOIL for all time steps +########################################################################################### + import rips import itertools import time @@ -5,16 +9,23 @@ import time resInsight = rips.Instance.find() start = time.time() -case = resInsight.project.case(id=0) -grid = case.grid(index = 0) +# Get the case with case id 0 +case = resInsight.project.case(id=0) + +# Get a list of all time steps timeSteps = case.timeSteps() averages = [] for i in range(0, len(timeSteps)): + # Get the results from time step i asynchronously + # It actually returns a generator object almost immediately resultChunks = case.properties.activeCellPropertyAsync('DYNAMIC_NATIVE', 'SOIL', i) mysum = 0.0 count = 0 + # Loop through and append the average. each time we loop resultChunks + # We will trigger a read of the input data, meaning the script will start + # Calculating averages before the whole resultValue for this time step has been received for chunk in resultChunks: mysum += sum(chunk.values) count += len(chunk.values) diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageNoComm.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageNoComm.py deleted file mode 100644 index 06b12c79d0..0000000000 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageNoComm.py +++ /dev/null @@ -1,16 +0,0 @@ -import sys -import os - -averages = [] -for i in range(0, 10): - values = [] - - sum = 0.0 - count = 0 - for j in range(0, 1199516): - sum += j - count += 1 - - averages.append(sum / count) - -print (averages) diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageSync.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageSync.py index 64da1939b4..7b3b879689 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageSync.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilAverageSync.py @@ -1,3 +1,6 @@ +########################################################################################### +# This example will synchronously calculate the average value for SOIL for all time steps +########################################################################################### import rips import itertools import time @@ -6,15 +9,17 @@ resInsight = rips.Instance.find() start = time.time() case = resInsight.project.case(id=0) -grid = case.grid(index = 0) +# Get the case with case id 0 +case = resInsight.project.case(id=0) + +# Get a list of all time steps timeSteps = case.timeSteps() averages = [] -allResults = [] for i in range(0, len(timeSteps)): + # Get a list of all the results for time step i results = case.properties.activeCellProperty('DYNAMIC_NATIVE', 'SOIL', i) - allResults.append(results) mysum = sum(results) averages.append(mysum/len(results)) diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilPorvAsync.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilPorvAsync.py index c9ba4b9e18..2c8bd954de 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilPorvAsync.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilPorvAsync.py @@ -1,32 +1,47 @@ +############################################################################## +# This example will create a derived result for each time step asynchronously +############################################################################## + import rips import time -import numpy as np +# Internal function for creating a result from a small chunk of soil and porv results +# The return value of the function is a generator for the results rather than the result itself. def createResult(soilChunks, porvChunks): for (soilChunk, porvChunk) in zip(soilChunks, porvChunks): resultChunk = [] number = 0 - npSoilChunk = np.array(soilChunk.values) - npPorvChunk = np.array(porvChunk.values) - yield npSoilChunk + npPorvChunk - + for (soilValue, porvValue) in zip(soilChunk.values, porvChunk.values): + resultChunk.append(soilValue * porvValue) + # Return a Python generator + yield resultChunk resInsight = rips.Instance.find() -start = time.time() +start = time.time() case = resInsight.project.case(id=0) timeStepInfo = case.timeSteps() +# Get a generator for the porv results. The generator will provide a chunk each time it is iterated porvChunks = case.properties.activeCellPropertyAsync('STATIC_NATIVE', 'PORV', 0) + +# Read the static result into an array, so we don't have to transfer it for each iteration +# Note we use the async method even if we synchronise here, because we need the values chunked +# ... to match the soil chunks porvArray = [] for porvChunk in porvChunks: porvArray.append(porvChunk) for i in range (0, len(timeStepInfo)): + # Get a generator object for the SOIL property for time step i soilChunks = case.properties.activeCellPropertyAsync('DYNAMIC_NATIVE', 'SOIL', i) - input_iterator = createResult(soilChunks, iter(porvArray)) - case.properties.setActiveCellPropertyAsync(input_iterator, 'GENERATED', 'SOILPORVAsync', i) + # Create the generator object for the SOIL * PORV derived result + result_generator = createResult(soilChunks, iter(porvArray)) + # Send back the result asynchronously with a generator object + case.properties.setActiveCellPropertyAsync(result_generator, 'GENERATED', 'SOILPORVAsync', i) end = time.time() print("Time elapsed: ", end - start) -print("Transferred all results back") \ No newline at end of file +print("Transferred all results back") + +view = case.views()[0].applyCellResult('GENERATED', 'SOILPORVAsync') \ No newline at end of file diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilPorvSync.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilPorvSync.py index 859a4f92bc..53684a211b 100644 --- a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilPorvSync.py +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/SoilPorvSync.py @@ -1,3 +1,7 @@ +############################################################################## +# This example will create a derived result for each time step synchronously +############################################################################## + import rips import time @@ -5,18 +9,25 @@ resInsight = rips.Instance.find() start = time.time() case = resInsight.project.case(id=0) +# Read the full porv result porvResults = case.properties.activeCellProperty('STATIC_NATIVE', 'PORV', 0) timeStepInfo = case.timeSteps() for i in range (0, len(timeStepInfo)): + # Read the full SOIl result for time step i soilResults = case.properties.activeCellProperty('DYNAMIC_NATIVE', 'SOIL', i) + + # Generate the result by looping through both lists in order results = [] for (soil, porv) in zip(soilResults, porvResults): results.append(soil * porv) + # Send back result case.properties.setActiveCellProperty(results, 'GENERATED', 'SOILPORVSync', i) end = time.time() print("Time elapsed: ", end - start) -print("Transferred all results back") \ No newline at end of file +print("Transferred all results back") + +view = case.views()[0].applyCellResult('GENERATED', 'SOILPORVSync') \ No newline at end of file diff --git a/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ViewExample.py b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ViewExample.py new file mode 100644 index 0000000000..63d70a76d5 --- /dev/null +++ b/ApplicationCode/GrpcInterface/Python/rips/PythonExamples/ViewExample.py @@ -0,0 +1,21 @@ +############################################################# +# This example will alter the views of all cases +# By setting the background color and toggle the grid box +############################################################# +import rips +# Connect to ResInsight instance +resInsight = rips.Instance.find() + +# Check if connection worked +if resInsight is not None: + # Get a list of all cases + cases = resInsight.project.cases() + for case in cases: + # Get a list of all views + views = case.views() + for view in views: + # Set some parameters for the view + view.setShowGridBox(not view.showGridBox()) + view.setBackgroundColor("#3388AA") + # Update the view in ResInsight + view.update() \ No newline at end of file