diff --git a/examples/Solver_Features/Adaptive_CFL/ONERAM6/Flow360.json b/examples/Solver_Features/Adaptive_CFL/ONERAM6/Flow360.json new file mode 100644 index 0000000..b2c1afd --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/ONERAM6/Flow360.json @@ -0,0 +1,68 @@ +{ + "geometry": { + "meshName": "wing_mixed_ph.2.lb8.ugrid", + "endianness": "little", + "refArea": 1.15315084119231, + "momentCenter": [ + 0, + 0, + 0 + ], + "momentLength": [ + 0.801672958512342, + 0.801672958512342, + 0.801672958512342 + ] + }, + "volumeOutput": { + "outputFormat": "paraview", + "outputFields": ["primitiveVars", "Mach", "qcriterion"] + }, + "surfaceOutput": { + "outputFormat": "paraview", + "outputFields": ["primitiveVars", "Cp", "Cf", "CfVec", "yPlus"] + }, + "navierStokesSolver": { + "absoluteTolerance": 1e-10, + "linearIterations": 25, + "kappaMUSCL": -1, + "limitVelocity": false, + "limitPressureDensity": false + }, + "turbulenceModelSolver": { + "modelType": "SpalartAllmaras", + "absoluteTolerance": 1e-10, + "linearIterations": 20, + "kappaMUSCL": -1, + "rotationCorrection": false + }, + "freestream": { + "Reynolds": 14600000, + "Mach": 0.84, + "Temperature": 288.15, + "alphaAngle": 3.06, + "betaAngle": 0 + }, + "timeStepping": { + "timeStepSize": "inf", + "physicalSteps": 1, + "maxPseudoSteps": 4000, + "CFL": { + "initial": 10, + "final": 100, + "rampSteps": 200, + "type": "ramp" + } + }, + "boundaries": { + "1": { + "type": "NoSlipWall" + }, + "2": { + "type": "SlipWall" + }, + "3": { + "type": "Freestream" + } + } +} diff --git a/examples/Solver_Features/Adaptive_CFL/ONERAM6/README.md b/examples/Solver_Features/Adaptive_CFL/ONERAM6/README.md new file mode 100644 index 0000000..7717ae1 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/ONERAM6/README.md @@ -0,0 +1,9 @@ +The following example demonstrates the use of adaptive CFL for the ONERAM6 wing + +To run the demo case follow these steps: + +1. Run `python submit_cases_from_downloads.py` -> this script will download the ONERAM6 mesh, and upload it to the Flexcompute servers. The cases will also be submitted for both adaptive and ramp CFL at two angles of attack. Alternatively the script `submit_cases_from_id.py` can also be used to run the cases from meshes already uploaded to Flexcompute servers. + +2. Run `python download_data.py` -> this script will download the csv files containing the loads and residual histories at two angles of attack + +3. Run `python convergence_plots_3p06.py` and `python convergence_plots_10.py` -> these scripts will cross-plot the load and residual convergence histories for the ramp and adaptive CFL cases at two angles of attack. diff --git a/examples/Solver_Features/Adaptive_CFL/ONERAM6/convergence_plots_10.py b/examples/Solver_Features/Adaptive_CFL/ONERAM6/convergence_plots_10.py new file mode 100644 index 0000000..30e0c5a --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/ONERAM6/convergence_plots_10.py @@ -0,0 +1,89 @@ +"""Plot the loads and residuals convergence plots for the ramp and adaptive CFL cases at 10 degrees""" + +import os + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +loads = ["CL", "CD"] +residuals = ["0_cont", "1_momx", "2_momy", "3_momz", "4_energ", "5_nuHat"] +figures = [] +axes = [] + + +def plot_loads(data, plot_name): + """Plot the loads""" + x = data["pseudo_step"] + for ax, load in zip(axes, loads): + ax.plot(x, data[load], label=plot_name) + ax.set_ylabel(load) + ax.legend() + ax.set_xlabel("Pseudo step") + ax.grid(which="major", color="gray") + if load == "CL": + ax.set_ylim(0.45, 0.8) + elif load == "CD": + ax.set_ylim(0.1, 0.15) + + +def plot_residuals(data, plot_name): + """Plot the residuals""" + x = data["pseudo_step"] + for ax, res in zip(axes, residuals): + for res in residuals: + if "RAMP" in plot_name: + ax.semilogy(x, data[res], label=plot_name + " " + res) + else: + ax.semilogy(x, data[res], "--", label=plot_name + " " + res) + ax.set_ylabel("Residuals") + ax.legend(fontsize="8") + ax.set_title("ONERAM6 -10 deg") + ax.grid(which="major", color="gray") + ax.set_xlabel("Pseudo step") + ax.set_yticks(10.0 ** np.arange(-14, -3)) + ax.set_ylim([1e-10, 1e-3]) + ax.set_xlim([0, 8000]) + + +# set output directory +dir_path = os.path.join(os.getcwd(), "figures") +os.makedirs(dir_path, exist_ok=True) + +# initialize figures & axes +for load in loads: + fig, ax = plt.subplots(figsize=(8, 6)) + figures.append(fig) + axes.append(ax) + +# calculate and plot loads convergence histories +for case_name in case_name_list: + if "10" in case_name: + csv_path = os.path.join(os.getcwd(), f"{case_name}", "total_forces_v2.csv") + data = pd.read_csv(csv_path, skipinitialspace=True) + plot_loads(data, case_name) + +for i, load in enumerate(loads): + figures[i].savefig(os.path.join(dir_path, load + "_AoA10.png"), dpi=500) + +for ax in axes: + ax.cla() + + +# plot residual convergence histories +fig, ax = plt.subplots(figsize=(8, 6)) +figures.append(fig) +axes.append(ax) + +for case_name in case_name_list: + if "10" in case_name: + if "ADAPTIVE" in case_name: + figures[0].gca().set_prop_cycle(None) + csv_path = os.path.join(os.getcwd(), case_name, "nonlinear_residual_v2.csv") + data = pd.read_csv(csv_path, skipinitialspace=True) + plot_residuals(data, case_name) + +figures[0].savefig(os.path.join(dir_path, "Residuals_AoA10.png"), dpi=500) diff --git a/examples/Solver_Features/Adaptive_CFL/ONERAM6/convergence_plots_3p06.py b/examples/Solver_Features/Adaptive_CFL/ONERAM6/convergence_plots_3p06.py new file mode 100644 index 0000000..4e3bedf --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/ONERAM6/convergence_plots_3p06.py @@ -0,0 +1,89 @@ +"""Plot the loads and residuals convergence plots for the ramp and adaptive CFL cases at 3.06 degrees""" + +import os + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +loads = ["CL", "CD"] +residuals = ["0_cont", "1_momx", "2_momy", "3_momz", "4_energ", "5_nuHat"] +figures = [] +axes = [] + + +def plot_loads(data, plot_name): + """Plot the loads""" + x = data["pseudo_step"] + for ax, load in zip(axes, loads): + ax.plot(x, data[load], label=plot_name) + ax.set_ylabel(load) + ax.legend() + ax.set_xlabel("Pseudo step") + ax.grid(which="major", color="gray") + if load == "CL": + ax.set_ylim(0.25, 0.3) + elif load == "CD": + ax.set_ylim(0.0, 0.05) + + +def plot_residuals(data, plot_name): + """Plot the residuals""" + x = data["pseudo_step"] + for ax, res in zip(axes, residuals): + for res in residuals: + if "RAMP" in plot_name: + ax.semilogy(x, data[res], label=plot_name + " " + res) + else: + ax.semilogy(x, data[res], "--", label=plot_name + " " + res) + ax.set_ylabel("Residuals") + ax.legend(fontsize="8") + ax.set_title("ONERAM6 -3.06 deg") + ax.grid(which="major", color="gray") + ax.set_xlabel("Pseudo step") + ax.set_yticks(10.0 ** np.arange(-14, -3)) + ax.set_ylim([1e-10, 1e-3]) + ax.set_xlim([0, 4000]) + + +# set output directory +dir_path = os.path.join(os.getcwd(), "figures") +os.makedirs(dir_path, exist_ok=True) + +# initialize figures & axes +for load in loads: + fig, ax = plt.subplots(figsize=(8, 6)) + figures.append(fig) + axes.append(ax) + +# calculate and plot loads convergence histories +for case_name in case_name_list: + if "3p06" in case_name: + csv_path = os.path.join(os.getcwd(), f"{case_name}", "total_forces_v2.csv") + data = pd.read_csv(csv_path, skipinitialspace=True) + plot_loads(data, case_name) + +for i, load in enumerate(loads): + figures[i].savefig(os.path.join(dir_path, load + "_AoA3p06.png"), dpi=500) + +for ax in axes: + ax.cla() + + +# plot residual convergence histories +fig, ax = plt.subplots(figsize=(8, 6)) +figures.append(fig) +axes.append(ax) + +for case_name in case_name_list: + if "3p06" in case_name: + if "ADAPTIVE" in case_name: + figures[0].gca().set_prop_cycle(None) + csv_path = os.path.join(os.getcwd(), case_name, "nonlinear_residual_v2.csv") + data = pd.read_csv(csv_path, skipinitialspace=True) + plot_residuals(data, case_name) + +figures[0].savefig(os.path.join(dir_path, "Residuals_AoA3p06.png"), dpi=500) diff --git a/examples/Solver_Features/Adaptive_CFL/ONERAM6/download_data.py b/examples/Solver_Features/Adaptive_CFL/ONERAM6/download_data.py new file mode 100644 index 0000000..2429730 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/ONERAM6/download_data.py @@ -0,0 +1,29 @@ +"""Download the results for the ramp and adaptive CFL cases at both angles of attack""" + +import os + +from flow360 import MyCases + +# read in case_name_list +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +my_cases = MyCases(limit=None) + +case = None + +for case_name in case_name_list: + case_folder = os.path.join(os.getcwd(), case_name) + os.makedirs(case_folder, exist_ok=True) + # find the latest case with the name corresponding to the name in case_name_list + for case in my_cases: + if case.name == case_name: + break + print(case.name) + # download the files + case.results.download( + nonlinear_residuals=True, + surface_forces=True, + total_forces=True, + destination=case_folder, + ) diff --git a/examples/Solver_Features/Adaptive_CFL/ONERAM6/submit_cases_from_downloads.py b/examples/Solver_Features/Adaptive_CFL/ONERAM6/submit_cases_from_downloads.py new file mode 100644 index 0000000..2876571 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/ONERAM6/submit_cases_from_downloads.py @@ -0,0 +1,64 @@ +"""Case submission through mesh download and upload onto Flexcompute servers""" + +import os.path +from urllib.request import urlretrieve + +import flow360 as fl + +case_name_list = [] + +# download meshes to the current directory + +URL = "PLACEHOLDER" +MESH_FILENAME = "mesh.lb8.ugrid" + +if not os.path.isfile(MESH_FILENAME): + urlretrieve(URL, MESH_FILENAME) + + +# submit mesh +volume_mesh = fl.VolumeMesh.from_file(MESH_FILENAME, name="ONERAM6_Mesh") +volume_mesh = volume_mesh.submit() + +params = fl.Flow360Params("Flow360.json") + +# submit ramp CFL case at alpha = 3.06 +case = fl.Case.create("ONERAM6_3p06_RAMP", params, volume_mesh.id) +case_name_list.append(case.name) +case = case.submit() + +# change CFL type to adaptive +params.time_stepping.CFL.type = "adaptive" + +# submit adaptive CFL case at alpha = 3.06 +case2 = fl.Case.create("ONERAM6_3p06_ADAPTIVE", params, volume_mesh.id) +case_name_list.append(case2.name) +case2 = case2.submit() + +# change CFL type to ramp, modify time stepping settings and increase alpha to 10 +params.time_stepping.CFL.type = "ramp" +params.time_stepping.max_pseudo_steps = 8000 +params.time_stepping.CFL.initial = 1 +params.time_stepping.CFL.final = 20 +params.time_stepping.CFL.ramp_steps = 200 +params.freestream.alpha = 10 + +# submit ramp CFL case at alpha = 10 +case3 = fl.Case.create("ONERAM6_10_RAMP", params, volume_mesh.id) +case_name_list.append(case3.name) +case3 = case3.submit() + + +# change CFL type to adaptive +params.time_stepping.CFL.type = "adaptive" + +# submit adaptive CFL case at alpha = 10 +case4 = fl.Case.create("ONERAM6_10_ADAPTIVE", params, volume_mesh.id) +case_name_list.append(case4.name) +case4 = case4.submit() + +# dump case names for use in download and post-processing scripts + +with open("case_name_list.dat", "w", encoding="utf-8") as f: + for line in case_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Adaptive_CFL/ONERAM6/submit_cases_from_id.py b/examples/Solver_Features/Adaptive_CFL/ONERAM6/submit_cases_from_id.py new file mode 100644 index 0000000..5274525 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/ONERAM6/submit_cases_from_id.py @@ -0,0 +1,50 @@ +"""Case submission from mesh ID's present on the Flexcompute servers""" + +import flow360 as fl + +case_name_list = [] + +MESH_ID = "3849aa75-56f7-4921-ae0e-8b5cf45ed58a" + +params = fl.Flow360Params("Flow360.json") + +# submit ramp CFL case at alpha = 3.06 +case = fl.Case.create("ONERAM6_3p06_RAMP", params, MESH_ID) +case_name_list.append(case.name) +case = case.submit() + +# change CFL type to adaptive +params.time_stepping.CFL.type = "adaptive" + +# submit adaptive CFL case at alpha = 3.06 +case2 = fl.Case.create("ONERAM6_3p06_ADAPTIVE", params, MESH_ID) +case_name_list.append(case2.name) +case2 = case2.submit() + +# change CFL type to ramp, modify time stepping settings and increase alpha to 10 +params.time_stepping.CFL.type = "ramp" +params.time_stepping.max_pseudo_steps = 8000 +params.time_stepping.CFL.initial = 1 +params.time_stepping.CFL.final = 20 +params.time_stepping.CFL.ramp_steps = 200 +params.freestream.alpha = 10 + +# submit ramp CFL case at alpha = 10 +case3 = fl.Case.create("ONERAM6_10_RAMP", params, MESH_ID) +case_name_list.append(case3.name) +case3 = case3.submit() + + +# change CFL type to adaptive +params.time_stepping.CFL.type = "adaptive" + +# submit adaptive CFL case at alpha = 10 +case4 = fl.Case.create("ONERAM6_10_ADAPTIVE", params, MESH_ID) +case_name_list.append(case4.name) +case4 = case4.submit() + +# dump case names for use in download and post-processing scripts + +with open("case_name_list.dat", "w", encoding="utf-8") as f: + for line in case_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Adaptive_CFL/XV15/Flow360.json b/examples/Solver_Features/Adaptive_CFL/XV15/Flow360.json new file mode 100644 index 0000000..16e2eb1 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/XV15/Flow360.json @@ -0,0 +1,86 @@ +{ + "geometry": { + "refArea": 70685.83470577035, + "momentCenter": [ + 0, + 0, + 0 + ], + "momentLength": [ + 150, + 150, + 150 + ] + }, + "volumeOutput": { + "outputFormat": "paraview", + "outputFields" : ["primitiveVars", "qcriterion", "Mach"] + }, + "surfaceOutput": { + "outputFormat": "paraview", + "outputFields" : ["primitiveVars", "Cp", "Cf", "CfVec", "yPlus"] + }, + "navierStokesSolver": { + "absoluteTolerance": 1e-9, + "relativeTolerance": 0.01, + "linearIterations": 35, + "kappaMUSCL": -1, + "orderOfAccuracy": 1, + "updateJacobianFrequency": 1, + "equationEvalFrequency": 1 + }, + "turbulenceModelSolver": { + "modelType": "SpalartAllmaras", + "absoluteTolerance": 1e-8, + "relativeTolerance": 0.01, + "linearIterations": 35, + "DDES": true, + "orderOfAccuracy": 1, + "updateJacobianFrequency": 1, + "equationEvalFrequency": 1, + "rotationCorrection": true + }, + "freestream": { + "muRef": 0.00000168, + "Mach": 0.182, + "MachRef": 0.54, + "Temperature": 288.15, + "alphaAngle": -90, + "betaAngle": 0 + }, + "boundaries": { + "stationaryField/farfield": { + "type": "Freestream" + }, + "rotationField/blade": { + "type": "NoSlipWall" + }, + "rotationField/blade_2": { + "type": "NoSlipWall" + }, + "rotationField/blade_3": { + "type": "NoSlipWall" + } + }, + "volumeZones":{ + "rotationField":{ + "modelType": "FluidDynamics", + "referenceFrame":{ + "axisOfRotation" : [0,0,-1], + "centerOfRotation" : [0,0,0], + "omega" : 0.0036 + } + } + }, + "timeStepping": { + "timeStepSize": 29.08882086657216, + "physicalSteps": 120, + "maxPseudoSteps": 13, + "CFL": { + "type": "ramp", + "initial": 1, + "final": 1000, + "rampSteps": 11 + } + } +} diff --git a/examples/Solver_Features/Adaptive_CFL/XV15/README.md b/examples/Solver_Features/Adaptive_CFL/XV15/README.md new file mode 100644 index 0000000..40036c7 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/XV15/README.md @@ -0,0 +1,9 @@ +The following example demonstrates the use of adaptive CFL for the XV15 rotor. + +To run the demo case follow these steps: + +1. Run `python submit_cases_from_downloads.py` -> this script will download the XV15 rotor mesh, and upload it to the Flexcompute servers. The cases will also be submitted for both adaptive and ramp CFL. Alternatively the script `submit_cases_from_id.py` can also be used to run the cases from meshes already uploaded to Flexcompute servers. + +2. Run `python download_data.py` -> this script will download the csv files containing the loads and residual histories. + +3. Run `python convergence_plots.py` to cross-plot the residual convergence histories for the ramp and adaptive CFL cases. diff --git a/examples/Solver_Features/Adaptive_CFL/XV15/convergence_plots.py b/examples/Solver_Features/Adaptive_CFL/XV15/convergence_plots.py new file mode 100644 index 0000000..5f322d2 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/XV15/convergence_plots.py @@ -0,0 +1,68 @@ +"""Plot the residuals convergence plots for the ramp and adaptive CFL cases""" + +import os + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +residuals = ["0_cont", "1_momx", "2_momy", "3_momz", "4_energ", "5_nuHat"] +figures = [] +axes = [] + + +def add_accum_pseudo_step(history): + "Compute the accumulated pseudo-step" + pseudo_steps = history["pseudo_step"] + accum = [] + accum.append(0) + for i in range(1, len(pseudo_steps)): + if pseudo_steps[i] > pseudo_steps[i - 1]: + accum.append(accum[-1] + abs(pseudo_steps[i] - pseudo_steps[i - 1])) + else: + accum.append(accum[-1] + 1) + history["accum_pseudo_step"] = accum + + +def plot_residuals(data, plot_name): + "Plot the residuals" + x = data["accum_pseudo_step"] + for ax, res in zip(axes, residuals): + for res in residuals: + if "ramp" in plot_name: + ax.semilogy(x, data[res], label=plot_name + " " + res) + else: + ax.semilogy(x, data[res], "--", label=plot_name + " " + res) + + ax.legend(loc="upper right") + ax.grid(which="major", color="gray") + ax.set_title("XV15 Rotor - Residuals") + ax.set_xlabel("Accumulated Pseudo step") + ax.set_yticks(10.0 ** np.arange(-11, -3)) + ax.set_ylim([1e-9, 1e-3]) + ax.set_xlim([3000, 4000]) + ax.set_ylabel("Residuals") + + +# set output directory +dir_path = os.path.join(os.getcwd(), "figures") +os.makedirs(dir_path, exist_ok=True) + +fig, ax = plt.subplots(figsize=(8, 6)) +figures.append(fig) +axes.append(ax) + +for case_name in case_name_list: + if "2nd_order" in case_name: + if "adaptive" in case_name: + figures[0].gca().set_prop_cycle(None) + csv_path = os.path.join(os.getcwd(), case_name, "nonlinear_residual_v2.csv") + data = pd.read_csv(csv_path, skipinitialspace=True) + add_accum_pseudo_step(data) + plot_residuals(data, case_name) +figures[0].savefig(os.path.join(dir_path, "residuals.png"), dpi=500) +axes[0].set_xlim([3450, 3550]) +figures[0].savefig("figures/residuals_zoomed.png", dpi=500) diff --git a/examples/Solver_Features/Adaptive_CFL/XV15/download_data.py b/examples/Solver_Features/Adaptive_CFL/XV15/download_data.py new file mode 100644 index 0000000..749fef1 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/XV15/download_data.py @@ -0,0 +1,27 @@ +"""Download the results for the ramp and adaptive CFL cases at both angles of attack""" + +import os + +from flow360 import MyCases + +# read in case_name_list +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +my_cases = MyCases(limit=None) +case = None +for case_name in case_name_list: + case_folder = os.path.join(os.getcwd(), case_name) + os.makedirs(case_folder, exist_ok=True) + # find the latest case with the name corresponding to the name in case_name_list + for case in my_cases: + if case.name == case_name: + break + print(case.name) + # download the files + case.results.download( + nonlinear_residuals=True, + surface_forces=True, + total_forces=True, + destination=case_folder, + ) diff --git a/examples/Solver_Features/Adaptive_CFL/XV15/submit_cases_from_downloads.py b/examples/Solver_Features/Adaptive_CFL/XV15/submit_cases_from_downloads.py new file mode 100644 index 0000000..999cea5 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/XV15/submit_cases_from_downloads.py @@ -0,0 +1,62 @@ +"""Case submission through mesh download and upload onto Flexcompute servers""" + +import os.path +from urllib.request import urlretrieve + +import flow360 as fl + +case_name_list = [] + +# download meshes to the current directory + +URL = "PLACEHOLDER" +MESH_FILENAME = "xv15_airplane_pitch26.cgns" + +if not os.path.isfile(MESH_FILENAME): + urlretrieve(URL, MESH_FILENAME) + + +# submit mesh +volume_mesh = fl.VolumeMesh.from_file(MESH_FILENAME, name="XV15_Mesh") +volume_mesh = volume_mesh.submit() + +params = fl.Flow360Params("Flow360.json") + +# submit 1st order case +case = fl.Case.create("XV15_1st_order_ramp", params, volume_mesh.id) +case_name_list.append(case.name) +case = case.submit() + +# update solver and time-stepping parameters +params.navier_stokes_solver.order_of_accuracy = 2 +params.turbulence_model_solver.order_of_accuracy = 2 + +params.time_stepping.max_pseudo_steps = 35 +params.time_stepping.time_step_size = 29.08882086657216 + +params.time_stepping.CFL.final = 1e7 +params.time_stepping.CFL.ramp_steps = 33 + + +# submit 2nd order ramp case (forked from first order) +case_fork_1 = case.fork() +case_fork_1.name = "XV15_2nd_order_ramp" +case_name_list.append(case_fork_1.name) +case_fork_1.params = params +case_fork_1 = case_fork_1.submit() + +# change CFL type to adaptive +params.time_stepping.CFL.type = "adaptive" + + +# submit adaptive CFL case +case2 = fl.Case.create("XV15_2nd_order_adaptive", params, volume_mesh.id) +case_name_list.append(case2.name) +case2 = case2.submit() + + +# dump case names for use in download and post-processing scripts + +with open("case_name_list.dat", "w", encoding="utf-8") as f: + for line in case_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Adaptive_CFL/XV15/submit_cases_from_id.py b/examples/Solver_Features/Adaptive_CFL/XV15/submit_cases_from_id.py new file mode 100644 index 0000000..a4ed885 --- /dev/null +++ b/examples/Solver_Features/Adaptive_CFL/XV15/submit_cases_from_id.py @@ -0,0 +1,45 @@ +"""Case submission from mesh ID's present on the Flexcompute servers""" + +import flow360 as fl + +case_name_list = [] + +MESH_ID = "05f1b090-2e93-4032-b703-ca665c1f9700" + +# submit 1st order case +params = fl.Flow360Params("Flow360.json") +case = fl.Case.create("XV15_1st_order_ramp", params, MESH_ID) +case_name_list.append(case.name) +case = case.submit() + +# update solver and time-stepping parameters +params.navier_stokes_solver.order_of_accuracy = 2 +params.turbulence_model_solver.order_of_accuracy = 2 + +params.time_stepping.max_pseudo_steps = 35 +params.time_stepping.time_step_size = 29.08882086657216 + +params.time_stepping.CFL.final = 1e7 +params.time_stepping.CFL.ramp_steps = 33 + + +# submit 2nd order ramp case (forked from first order) +case_fork_1 = case.fork() +case_fork_1.name = "XV15_2nd_order_ramp" +case_name_list.append(case_fork_1.name) +case_fork_1.params = params +case_fork_1 = case_fork_1.submit() + +# change CFL type to adaptive +params.time_stepping.CFL.type = "adaptive" + +# submit adaptive CFL case +case2 = fl.Case.create("XV15_2nd_order_adaptive", params, MESH_ID) +case_name_list.append(case2.name) +case2 = case2.submit() + +# dump case names for use in download and post-processing scripts + +with open("case_name_list.dat", "w", encoding="utf-8") as f: + for line in case_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Debug_Divergence/Flow360_AD.json b/examples/Solver_Features/Debug_Divergence/Flow360_AD.json new file mode 100644 index 0000000..0a174a5 --- /dev/null +++ b/examples/Solver_Features/Debug_Divergence/Flow360_AD.json @@ -0,0 +1,89 @@ +{ + "geometry": { + "comments": { + "meshUnit": "inch" + }, + "refArea": 5, + "momentCenter": [ + 0.25, + 0.25, + 0.25 + ], + "momentLength": [ + 1, + 1, + 1 + ] + }, + "freestream": { + "muRef": 0.0000016899682361477025, + "Mach": 0.85, + "Temperature": 288.15, + "alphaAngle": 8, + "betaAngle": 0, + "comments": { + "pressure": 101325.009090375, + "densityKgPerCubicMeter": 1.225, + "speedOfSoundMeterPerSecond": 340.29400580821283, + "freestreamMeterPerSecond": 270, + "meshUnitInMeter": 0.0254 + } + }, + "boundaries": { + "fluid/wing": { + "type": "NoSlipWall" + }, + "fluid/farfield": { + "type": "Freestream" + }, + "fluid/symmetric": { + "type": "SlipWall" + } + }, + "timeStepping": { + "maxPseudoSteps": 5000, + "CFL": { + "type": "ramp", + "initial": 1, + "final": 200, + "rampSteps": 1000 + }, + "physicalSteps": 1, + "timeStepSize": "inf" + }, + "navierStokesSolver": { + "absoluteTolerance": 1e-9, + "linearIterations": 35, + "kappaMUSCL": -1, + "orderOfAccuracy": 2 + }, + "turbulenceModelSolver": { + "modelType": "SpalartAllmaras", + "rotationCorrection": false, + "absoluteTolerance": 1e-8, + "linearIterations": 25, + "kappaMUSCL": -1, + "orderOfAccuracy": 2 + }, + "volumeOutput": { + "outputFormat": "paraview", + "outputFields": ["primitiveVars", "Mach", "qcriterion"] + }, + "surfaceOutput": { + "outputFormat": "paraview", + "outputFields": ["Cp", "Cf", "CfVec", "yPlus"] + }, + "actuatorDisks": + [ + { + "center":[-1.0, 2.5, 0.0], + "axisThrust":[-1.0,0.0,0.0], + "thickness": 0.3, + "forcePerArea":{ + "radius":[0.01, 0.7, 0.75], + "thrust":[0.2, 0.5, 0], + "circumferential":[-0.001, -0.003, 0] + } + } + ] +} diff --git a/examples/Solver_Features/Debug_Divergence/Flow360_PoorSurface.json b/examples/Solver_Features/Debug_Divergence/Flow360_PoorSurface.json new file mode 100644 index 0000000..7ee57a5 --- /dev/null +++ b/examples/Solver_Features/Debug_Divergence/Flow360_PoorSurface.json @@ -0,0 +1,76 @@ +{ + "geometry": { + "comments": { + "meshUnit": "inch" + }, + "refArea": 5, + "momentCenter": [ + 0.25, + 0.25, + 0.25 + ], + "momentLength": [ + 1, + 1, + 1 + ] + }, + "freestream": { + "muRef": 0.0000016899682361477025, + "Mach": 0.85, + "Temperature": 288.15, + "alphaAngle": 8, + "betaAngle": 0, + "comments": { + "pressure": 101325.009090375, + "densityKgPerCubicMeter": 1.225, + "speedOfSoundMeterPerSecond": 340.29400580821283, + "freestreamMeterPerSecond": 270, + "meshUnitInMeter": 0.0254 + } + }, + "boundaries": { + "fluid/wing": { + "type": "NoSlipWall" + }, + "fluid/farfield": { + "type": "Freestream" + }, + "fluid/symmetric": { + "type": "SlipWall" + } + }, + "timeStepping": { + "maxPseudoSteps": 5000, + "CFL": { + "type": "ramp", + "initial": 1, + "final": 200, + "rampSteps": 1000 + }, + "physicalSteps": 1, + "timeStepSize": "inf" + }, + "navierStokesSolver": { + "absoluteTolerance": 1e-9, + "linearIterations": 35, + "kappaMUSCL": -1, + "orderOfAccuracy": 2 + }, + "turbulenceModelSolver": { + "modelType": "SpalartAllmaras", + "rotationCorrection": false, + "absoluteTolerance": 1e-8, + "linearIterations": 25, + "kappaMUSCL": -1, + "orderOfAccuracy": 2 + }, + "volumeOutput": { + "outputFormat": "paraview", + "outputFields": ["primitiveVars", "Mach", "qcriterion"] + }, + "surfaceOutput": { + "outputFormat": "paraview", + "outputFields": ["Cp", "Cf", "CfVec", "yPlus"] + } +} diff --git a/examples/Solver_Features/Debug_Divergence/README.md b/examples/Solver_Features/Debug_Divergence/README.md new file mode 100644 index 0000000..fd5faf0 --- /dev/null +++ b/examples/Solver_Features/Debug_Divergence/README.md @@ -0,0 +1,27 @@ +The following cases are for a demonstration of the debug divergence pipeline and are intended to diverge. + +To run the demo case follow these steps: + +1. Run `python submit_cases_from_downloads.py` -> this script will download both wall resolved and wall model meshes, and upload them to the Flexcompute servers. The cases will also be submitted. Alternatively the script `submit_cases_from_id.py` can also be used to run the cases from meshes already uploaded to Flexcompute servers. + +2. Run `python download_data.py` -> this script will download the csv files containing the min/max locations of the pressure, density, velocity magnitude and turbulent variables. + +3. Run `python print_divergence_locations.py` -> this script will print the minimum pressure, minimum density, maxiumum velocity magnitude and minimum nuHat at the last iteration. + +4. Investigate the mesh at the location, where one of the variables went non-physical (e.g. negative pressure or density) + +The debug divergence pipeline can also be used on the WebUI: + +1. Go to min/max tab and position cursor on last pseudo-step in plot. Read which variable diverged from plot or from table below. + +2. Go to case visualization tab. + +3. Turn-off all boundaries and slices + +4. Switch on non-slip walls. + +5. Switch on slices of interest for the diverged variable. + +6. Investigated multiple slices and views (flat vs crinkle vs wireframe with contour on/off) + +7. Examine the mesh for quality issues such as large jumps in area/volume ratio's diff --git a/examples/Solver_Features/Debug_Divergence/download_data.py b/examples/Solver_Features/Debug_Divergence/download_data.py new file mode 100644 index 0000000..ce6775b --- /dev/null +++ b/examples/Solver_Features/Debug_Divergence/download_data.py @@ -0,0 +1,27 @@ +"""Download the min/max locations for the poor surface mesh CRM and actuator disk cases""" + +import os + +from flow360 import MyCases + +# read in case_name_list +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +my_cases = MyCases(limit=None) + +case = None + +for case_name in case_name_list: + case_folder = os.path.join(os.getcwd(), case_name) + os.makedirs(case_folder, exist_ok=True) + # find the latest case with the name corresponding to the name in case_name_list + for case in my_cases: + if case.name == case_name: + break + print(case.name) + # download the files + case.results.download( + minmax_state=True, + destination=case_folder, + ) diff --git a/examples/Solver_Features/Debug_Divergence/print_divergence_locations.py b/examples/Solver_Features/Debug_Divergence/print_divergence_locations.py new file mode 100644 index 0000000..8eaa02a --- /dev/null +++ b/examples/Solver_Features/Debug_Divergence/print_divergence_locations.py @@ -0,0 +1,45 @@ +"""Print the divergence locations for minimum density, pressure, eddy viscosity and maximum velocity magnitude""" + +import os + +import pandas as pd + +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +for case in case_name_list: + csv_path = os.path.join(os.getcwd(), case, "minmax_state_v2.csv") + data = pd.read_csv(csv_path, skipinitialspace=True) + print(case) + print( + "Minimum Density:{} at location xyz = {}, {}, {}".format( + data["min_rho"].values[-1], + data["min_rho_x"].values[-1], + data["min_rho_y"].values[-1], + data["min_rho_z"].values[-1], + ) + ) + print( + "Minimum Pressure: {} at location xyz = {}, {}, {}".format( + data["min_p"].values[-1], + data["min_p_x"].values[-1], + data["min_p_y"].values[-1], + data["min_p_z"].values[-1], + ) + ) + print( + "Maximum Velocity Magnitude: {} at location xyz = {}, {}, {}".format( + data["max_umag"].values[-1], + data["max_umag_x"].values[-1], + data["max_umag_y"].values[-1], + data["max_umag_z"].values[-1], + ) + ) + print( + "Minimum nuHat Magnitude: {} at location xyz = {}, {}, {}".format( + data["min_nuHat"].values[-1], + data["min_nuHat_x"].values[-1], + data["min_nuHat_y"].values[-1], + data["min_nuHat_z"].values[-1], + ) + ) diff --git a/examples/Solver_Features/Debug_Divergence/submit_cases_from_downloads.py b/examples/Solver_Features/Debug_Divergence/submit_cases_from_downloads.py new file mode 100644 index 0000000..c2f64c6 --- /dev/null +++ b/examples/Solver_Features/Debug_Divergence/submit_cases_from_downloads.py @@ -0,0 +1,48 @@ +"""Case submission through mesh download and upload onto Flexcompute servers""" + +import os.path +from urllib.request import urlretrieve + +import flow360 as fl + +case_name_list = [] + +# download meshes to the current directory + +URL = "PLACEHOLDER" +MESH_FILENAME = "Unswept_CRM_Poor_Surface.cgns" + +if not os.path.isfile(MESH_FILENAME): + urlretrieve(URL, MESH_FILENAME) + +URL2 = "PLACEHOLDER" +MESH_FILENAME2 = "Unswept_CRM_AD.cgns" + +if not os.path.isfile(MESH_FILENAME2): + urlretrieve(URL2, MESH_FILENAME2) + +# submit mesh with poor surface +volume_mesh = fl.VolumeMesh.from_file(MESH_FILENAME, name="CRM_Poor_Surface_Mesh") +volume_mesh = volume_mesh.submit() + +# submit mesh with actuator disk +volume_mesh2 = fl.VolumeMesh.from_file(MESH_FILENAME2, name="CRM_AD_Mesh") +volume_mesh2 = volume_mesh2.submit() + +# submit case with poor surface +params = fl.Flow360Params("Flow360_PoorSurface.json") +case = fl.Case.create("CRM_Poor_Surface", params, volume_mesh.id) +case_name_list.append(case.name) +case = case.submit() + +# submit case with actuator disk +params2 = fl.Flow360Params("Flow360_AD.json") +case2 = fl.Case.create("CRM_AD", params2, volume_mesh2.id) +case_name_list.append(case2.name) +case2 = case2.submit() + +# dump case names for use in download and post-processing scripts + +with open("case_name_list.dat", "w", encoding="utf-8") as f: + for line in case_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Debug_Divergence/submit_cases_from_id.py b/examples/Solver_Features/Debug_Divergence/submit_cases_from_id.py new file mode 100644 index 0000000..e7bf7bd --- /dev/null +++ b/examples/Solver_Features/Debug_Divergence/submit_cases_from_id.py @@ -0,0 +1,25 @@ +"""Case submission from mesh ID's present on the Flexcompute servers""" + +import flow360 as fl + +case_name_list = [] + +MESH_ID = "49d4b0aa-3a42-4879-8c9c-d977112e7665" +MESH_ID2 = "2c1fb0cb-36bc-47f9-a166-4a61507ebfc8" + +# submit case with poor surface +params = fl.Flow360Params("Flow360_PoorSurface.json") +case = fl.Case.create("CRM_Poor_Surface", params, MESH_ID) +case_name_list.append(case.name) +case = case.submit() + +# submit case with actuator disk +params2 = fl.Flow360Params("Flow360_AD.json") +case2 = fl.Case.create("CRM_AD", params2, MESH_ID2) +case_name_list.append(case2.name) +case2 = case2.submit() + +# dump case names for use in download and post-processing scripts +with open("case_name_list.dat", "w", encoding="utf-8") as f: + for line in case_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Mesh_Metrics/Flow360_PoorSurface.json b/examples/Solver_Features/Mesh_Metrics/Flow360_PoorSurface.json new file mode 100644 index 0000000..7ee57a5 --- /dev/null +++ b/examples/Solver_Features/Mesh_Metrics/Flow360_PoorSurface.json @@ -0,0 +1,76 @@ +{ + "geometry": { + "comments": { + "meshUnit": "inch" + }, + "refArea": 5, + "momentCenter": [ + 0.25, + 0.25, + 0.25 + ], + "momentLength": [ + 1, + 1, + 1 + ] + }, + "freestream": { + "muRef": 0.0000016899682361477025, + "Mach": 0.85, + "Temperature": 288.15, + "alphaAngle": 8, + "betaAngle": 0, + "comments": { + "pressure": 101325.009090375, + "densityKgPerCubicMeter": 1.225, + "speedOfSoundMeterPerSecond": 340.29400580821283, + "freestreamMeterPerSecond": 270, + "meshUnitInMeter": 0.0254 + } + }, + "boundaries": { + "fluid/wing": { + "type": "NoSlipWall" + }, + "fluid/farfield": { + "type": "Freestream" + }, + "fluid/symmetric": { + "type": "SlipWall" + } + }, + "timeStepping": { + "maxPseudoSteps": 5000, + "CFL": { + "type": "ramp", + "initial": 1, + "final": 200, + "rampSteps": 1000 + }, + "physicalSteps": 1, + "timeStepSize": "inf" + }, + "navierStokesSolver": { + "absoluteTolerance": 1e-9, + "linearIterations": 35, + "kappaMUSCL": -1, + "orderOfAccuracy": 2 + }, + "turbulenceModelSolver": { + "modelType": "SpalartAllmaras", + "rotationCorrection": false, + "absoluteTolerance": 1e-8, + "linearIterations": 25, + "kappaMUSCL": -1, + "orderOfAccuracy": 2 + }, + "volumeOutput": { + "outputFormat": "paraview", + "outputFields": ["primitiveVars", "Mach", "qcriterion"] + }, + "surfaceOutput": { + "outputFormat": "paraview", + "outputFields": ["Cp", "Cf", "CfVec", "yPlus"] + } +} diff --git a/examples/Solver_Features/Mesh_Metrics/README.md b/examples/Solver_Features/Mesh_Metrics/README.md new file mode 100644 index 0000000..ae982aa --- /dev/null +++ b/examples/Solver_Features/Mesh_Metrics/README.md @@ -0,0 +1,7 @@ +This following example demonstrates the mesh metrics feature for a poor mesh of the unswept CRM wing. + +To run the demo case perform one of the following steps: + +If the mesh is already uploaded on the flexcompute servers, run `python submit_case_from_mesh_id.py` -> this script will upload the case with a poor surface mesh, which will trigger mesh metrics validation warnings. + +Alternatively, the mesh can be uploaded using `python submit_mesh_from_download.py`. The case can then be uploaded using `python submit_case_from_mesh_name` Note that the mesh metrics can take a short amount of time to be calculated, once the mesh is uploaded. To check whether the mesh metrics are available, the following script can be ran: `python download_mesh_metrics`. diff --git a/examples/Solver_Features/Mesh_Metrics/download_mesh_metrics.py b/examples/Solver_Features/Mesh_Metrics/download_mesh_metrics.py new file mode 100644 index 0000000..1be929c --- /dev/null +++ b/examples/Solver_Features/Mesh_Metrics/download_mesh_metrics.py @@ -0,0 +1,16 @@ +"""Downloads the mesh metrics to verify that they have been calculated after mesh upload""" + +import flow360 as fl + +with open("mesh_name_list.dat", "r", encoding="utf-8") as file: + mesh_name_list = file.read().splitlines() + +mesh = None + +my_meshes = fl.MyVolumeMeshes(limit=None) +for mesh_name in mesh_name_list: + for mesh in my_meshes: + if mesh.name == mesh_name: + break + +mesh.download_file("metadata/meshMetrics.json", to_folder="./") diff --git a/examples/Solver_Features/Mesh_Metrics/meshMetrics.json b/examples/Solver_Features/Mesh_Metrics/meshMetrics.json new file mode 100644 index 0000000..995494c --- /dev/null +++ b/examples/Solver_Features/Mesh_Metrics/meshMetrics.json @@ -0,0 +1,317 @@ +{ + "boundaries": [ + { + "area": { + "arithmeticMean": 291.92082281439565, + "max": 473.2736511230469, + "maxElementTypes": "Triangle", + "maxLocation": [ + 147.55576534354338, + 173.443682294482, + 101.35589838513086 + ], + "min": 132.748291015625, + "minElementTypes": "Triangle", + "minLocation": [ + 63.28409037995478, + 226.93446344287602, + -82.99685725903372 + ] + }, + "areaRatio": { + "arithmeticMean": 1.4985113090976945, + "max": 2.681257724761963, + "maxElementTypes": [ + "Triangle", + "Triangle" + ], + "maxLocation": [ + [ + 41.22672636048258, + 235.12222480709738, + -72.30697082376679 + ], + [ + 63.28409037995478, + 226.93446344287602, + -82.99685725903372 + ] + ], + "min": 1.0005501508712769, + "minElementTypes": [ + "Triangle", + "Triangle" + ], + "minLocation": [ + [ + -216.8532168881018, + 5.701608820427729, + 123.08810546996047 + ], + [ + -206.0711587802807, + 5.701608820427729, + 140.50172722057076 + ] + ] + }, + "aspectRatio": { + "arithmeticMean": 1.2360145225757426, + "max": 2.1813117475558967, + "maxElementTypes": "Triangle", + "maxLocation": [ + 157.29089552318047, + 149.3299566269409, + 123.1598738884641 + ], + "min": 1.002476929672776, + "minElementTypes": "Triangle", + "minLocation": [ + -209.12872863786876, + 87.0875263617167, + 104.67364276064143 + ] + }, + "boundaryName": "fluid/farfield", + "firstLayerThickness": { + "arithmeticMean": 19.462266130911928, + "max": 30.74910545349121, + "maxElementTypes": "Node", + "maxLocation": [ + 20.125249633278905, + 248.82412762548896, + 13.473226733512927 + ], + "min": 9.850659370422363, + "minElementTypes": "Node", + "minLocation": [ + 77.96281986731935, + 64.44705885200133, + 228.62277953795996 + ] + }, + "nodeCount": 698 + }, + { + "area": { + "arithmeticMean": 1.945036356115944, + "max": 445.40167236328125, + "maxElementTypes": "Triangle", + "maxLocation": [ + 149.48777645821437, + 0.0, + 176.5591785237306 + ], + "min": 3.0505323744475787e-13, + "minElementTypes": "Triangle", + "minLocation": [ + 1.000000608340996, + 0.0, + 0.0014703500674634617 + ] + }, + "areaRatio": { + "arithmeticMean": 2.167965960269024, + "max": 7451.1826171875, + "maxElementTypes": [ + "Quadrilateral", + "Triangle" + ], + "maxLocation": [ + [ + 0.9991196802950424, + 0.0, + 0.0019439293803997222 + ], + [ + 1.000000608340996, + 0.0, + 0.0014703500674634617 + ] + ], + "min": 1.0005085468292236, + "minElementTypes": [ + "Quadrilateral", + "Quadrilateral" + ], + "minLocation": [ + [ + 0.9949392997459301, + 0.0, + 0.0004061822794153316 + ], + [ + 0.9930284808138277, + 0.0, + 0.0009942629024474203 + ] + ] + }, + "aspectRatio": { + "arithmeticMean": 155.74056106968388, + "max": 9645.000651435734, + "maxElementTypes": "Quadrilateral", + "maxLocation": [ + 0.0017712367717225902, + 0.0, + 0.004573968435596714 + ], + "min": 1.0013615647691088, + "minElementTypes": "Triangle", + "minLocation": [ + 1.332686192522447, + 0.0, + -0.09446793669282744 + ] + }, + "boundaryName": "fluid/symmetric", + "firstLayerThickness": { + "arithmeticMean": 0.20590281270261715, + "max": 23.850631713867188, + "maxElementTypes": "Node", + "maxLocation": [ + -238.23550010678917, + 0.0, + -75.7881685282609 + ], + "min": 4.633235221263021e-05, + "minElementTypes": "Node", + "minLocation": [ + -0.0007234002335295815, + 0.0, + -0.00033018886195446503 + ] + }, + "nodeCount": 73383 + }, + { + "area": { + "arithmeticMean": 0.00016775446368517011, + "max": 0.01729346439242363, + "maxElementTypes": "Quadrilateral", + "maxLocation": [ + 0.524345493855963, + 2.2948648494290977, + 0.06395318305183383 + ], + "min": 5.556775040105322e-09, + "minElementTypes": "Triangle", + "minLocation": [ + 0.0001067611691790419, + 4.999999999999999, + -0.00017025561013887778 + ] + }, + "areaRatio": { + "arithmeticMean": 2.6092350756744613, + "max": 16650.416015625, + "maxElementTypes": [ + "Triangle", + "Triangle" + ], + "maxLocation": [ + [ + 0.0012227912585606909, + 4.9799999999999995, + 0.003100949926446326 + ], + [ + 0.00012051441305754237, + 5.0, + -4.032887514092763e-06 + ] + ], + "min": 1.0, + "minElementTypes": [ + "Triangle", + "Triangle" + ], + "minLocation": [ + [ + 1.0, + 0.026248096390986523, + -0.00098 + ], + [ + 1.0, + 0.026248096390986523, + -0.00098 + ] + ] + }, + "aspectRatio": { + "arithmeticMean": 15.835971698502322, + "max": 3234.080179288396, + "maxElementTypes": "Quadrilateral", + "maxLocation": [ + 1.358701330455523e-06, + 2.294864849429104, + -2.4963051225807055e-05 + ], + "min": 1.0000652114477049, + "minElementTypes": "Triangle", + "minLocation": [ + 0.5409056017557239, + 0.0005078719426672653, + -0.0418308773330812 + ] + }, + "boundaryName": "fluid/wing", + "firstLayerThickness": { + "arithmeticMean": 9.94855714014816e-07, + "max": 1.0125497738044942e-06, + "maxElementTypes": "Node", + "maxLocation": [ + 0.9987884202543752, + 0.001092672346567293, + -0.0009659299906853389 + ], + "min": 9.339078399506207e-10, + "minElementTypes": "Node", + "minLocation": [ + 0.8860456275429734, + 4.992741128205036, + 0.028779738821366026 + ] + }, + "nodeCount": 34996 + } + ], + "version": "v1.0.0", + "volumetric": { + "aspectRatio": { + "arithmeticMean": 1514.9884540743383, + "max": 193869.71426557042, + "maxElementTypes": "Hexahedron", + "maxLocation": [ + 0.5243455076566441, + 2.294864849431958, + 0.06395368276418312 + ], + "min": 1.0350238270624377, + "minElementTypes": "Tetrahedron", + "minLocation": [ + 1.0387296013781655, + 5.056439364641543, + -0.03925651985503204 + ] + }, + "volume": { + "arithmeticMean": 6.561119480285893, + "max": 7237.24589751225, + "maxElementTypes": "Tetrahedron", + "maxLocation": [ + -17.706885592654487, + 175.4454219756072, + -92.69272185260752 + ], + "min": 7.638792269195606e-19, + "minElementTypes": "Prism", + "minLocation": [ + 1.0000011401438824, + 5.00000034892017, + 0.0014706859379563682 + ] + } + } +} diff --git a/examples/Solver_Features/Mesh_Metrics/submit_case_from_mesh_id.py b/examples/Solver_Features/Mesh_Metrics/submit_case_from_mesh_id.py new file mode 100644 index 0000000..1e73163 --- /dev/null +++ b/examples/Solver_Features/Mesh_Metrics/submit_case_from_mesh_id.py @@ -0,0 +1,21 @@ +"""Case submission from mesh ID's present on the Flexcompute servers""" + +import flow360 as fl + +mesh_name_list = [] + +MESH_ID = "49d4b0aa-3a42-4879-8c9c-d977112e7665" + +# submit case with poor surface +params = fl.Flow360Params("CRM_PoorSurface.json") +case = fl.Case.create("CRM_Poor_Surface", params, MESH_ID) +case = case.submit() + +# write the volume mesh name to a file +volume_mesh = fl.VolumeMesh(MESH_ID) + +mesh_name_list.append(volume_mesh.name) + +with open("mesh_name_list.dat", "w", encoding="utf-8") as f: + for line in mesh_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Mesh_Metrics/submit_case_from_mesh_name.py b/examples/Solver_Features/Mesh_Metrics/submit_case_from_mesh_name.py new file mode 100644 index 0000000..bdc3391 --- /dev/null +++ b/examples/Solver_Features/Mesh_Metrics/submit_case_from_mesh_name.py @@ -0,0 +1,18 @@ +"""Case submission from mesh name stored in mesh_name_list.dat""" + +import flow360 as fl + +with open("mesh_name_list.dat", "r", encoding="utf=8") as file: + mesh_name_list = file.read().splitlines() + +mesh = None +my_meshes = fl.MyVolumeMeshes(limit=None) +for mesh_name in mesh_name_list: + for mesh in my_meshes: + if mesh.name == mesh_name: + break + + # submit case with poor surface + params = fl.Flow360Params("Flow360_PoorSurface.json") + case = fl.Case.create("CRM_Poor_Surface", params, mesh.id) + case = case.submit() diff --git a/examples/Solver_Features/Mesh_Metrics/submit_mesh_from_download.py b/examples/Solver_Features/Mesh_Metrics/submit_mesh_from_download.py new file mode 100644 index 0000000..e3d7139 --- /dev/null +++ b/examples/Solver_Features/Mesh_Metrics/submit_mesh_from_download.py @@ -0,0 +1,26 @@ +"""Submits the mesh from mesh download and upload onto Flexcompute servers""" + +import os.path +from urllib.request import urlretrieve + +import flow360 as fl + +mesh_name_list = [] + +# download meshes to the current directory + +URL = "PLACEHOLDER" +MESH_FILENAME = "Unswept_CRM_Poor_Surface.cgns" + +if not os.path.isfile(MESH_FILENAME): + urlretrieve(URL, MESH_FILENAME) + + +# submit mesh with poor surface +volume_mesh = fl.VolumeMesh.from_file(MESH_FILENAME, name="CRM_Poor_Surface_Mesh") +volume_mesh = volume_mesh.submit() +mesh_name_list.append(volume_mesh.name) + +with open("mesh_name_list.dat", "w", encoding="utf-8") as f: + for line in mesh_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Wall_Model/Flow360.json b/examples/Solver_Features/Wall_Model/Flow360.json new file mode 100644 index 0000000..7b32fc5 --- /dev/null +++ b/examples/Solver_Features/Wall_Model/Flow360.json @@ -0,0 +1,88 @@ +{ + "geometry" : { + "refArea" : 0.056, + "momentCenter" : [0, 0, 0], + "momentLength" : [0.6375, 0.6375, 0.6375] + }, + "volumeOutput" : { + "outputFormat" : "paraview", + "outputFields" : ["primitiveVars", "qcriterion", "Mach"] + }, + "surfaceOutput" : { + "outputFormat" : "paraview", + "outputFields" : ["primitiveVars", "Cp", "Cf", "CfVec", "yPlus"] + }, + "navierStokesSolver" : { + "absoluteTolerance" : 1e-12, + "kappaMUSCL" : -1.0 + }, + "turbulenceModelSolver" : { + "modelType" : "SpalartAllmaras", + "absoluteTolerance" : 1e-10 + }, + "freestream": { + "muRef": 4.2925193198151646e-8, + "Mach": 0.11754541460405184, + "Temperature": 288.15, + "alphaAngle": 0, + "betaAngle": 0, + "comments": { + "pressure": 101325.009090375, + "densityKgPerCubicMeter": 1.225, + "speedOfSoundMeterPerSecond": 340.29400580821283, + "freestreamMeterPerSecond": 40, + "meshUnitInMeter": 1 + } + }, + "boundaries" : { + "1": { + "type": "NoSlipWall", + "name": "Flow.CFDWT.FloorUnder" + }, + "2": { + "type": "Freestream", + "name": "Flow.CFDWT.In" + }, + "3": { + "type": "SlipWall", + "name": "Flow.CFDWT.Left" + }, + "4": { + "type": "NoSlipWall", + "name": "Flow.CFDWT.Floor" + }, + "5": { + "type": "SlipWall", + "name": "Flow.CFDWT.Roof" + }, + "6": { + "type": "Freestream", + "name": "Flow.CFDWT.Out" + }, + "7": { + "type": "SlipWall", + "name": "symmetry" + }, + "8": { + "type": "NoSlipWall", + "name": "Windsor" + }, + "9": { + "type": "NoSlipWall", + "name": "Windsor_rear" + }, + "10": { + "type": "NoSlipWall", + "name": "Windsor_supports" + } + }, + "timeStepping" : { + "maxPseudoSteps" : 10000, + "CFL" : { + "type": "ramp", + "initial" : 1, + "final": 200, + "rampSteps" : 2000 + } + } +} diff --git a/examples/Solver_Features/Wall_Model/README.md b/examples/Solver_Features/Wall_Model/README.md new file mode 100644 index 0000000..19ee98b --- /dev/null +++ b/examples/Solver_Features/Wall_Model/README.md @@ -0,0 +1,13 @@ +The following example compares wall-resolved and wall-modeled simulations for the Windsor body. + +To run the demo case follow these steps: + +1. Run `python submit_cases_from_downloads.py` -> this script will download both wall resolved and wall model meshes, and upload them to the Flexcompute servers. The cases will also be submitted. Alternatively the script `submit_cases_from_id.py` can also be used to run the cases from meshes already uploaded to Flexcompute servers. + +2. Run `python download_data.py` -> this script will download the csv files containing the loads and residual histories. + +3. Run `python convergence_plots.py` -> this script will cross-plot the load and residual convergence histories for the wall-resolved and wall-modeled cases. + + + + diff --git a/examples/Solver_Features/Wall_Model/convergence_plots.py b/examples/Solver_Features/Wall_Model/convergence_plots.py new file mode 100644 index 0000000..f110a8a --- /dev/null +++ b/examples/Solver_Features/Wall_Model/convergence_plots.py @@ -0,0 +1,94 @@ +"""Plot the loads and residuals convergence plots for the wall-resolved and wall-modeled cases""" + +import os + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +loads = ["CL", "CD"] +residuals = ["0_cont", "1_momx", "2_momy", "3_momz", "4_energ", "5_nuHat"] +figures = [] +axes = [] + + +def compute_cl_cd(data): + """Compute the Windsor body lift and drag from surface forces""" + + cl_0 = np.add(data["Windsor_CL"], data["Windsor_rear_CL"]) + data["CL"] = np.add(data["Windsor_supports_CL"], cl_0) + cd_0 = np.add(data["Windsor_CD"], data["Windsor_rear_CD"]) + data["CD"] = np.add(data["Windsor_supports_CD"], cd_0) + + +def plot_loads(data, plot_name): + """Plot the loads""" + + x = data["pseudo_step"] + for ax, load in zip(axes, loads): + ax.plot(x, data[load], label=plot_name) + ax.set_ylabel(load) + ax.legend() + ax.set_xlabel("Pseudo step") + ax.grid(which="major", color="gray") + if load == "CL": + ax.set_ylim(-0.2, -0.1) + elif load == "CD": + ax.set_ylim(0.25, 0.45) + + +def plot_residuals(data, plot_name): + """Plot the residuals""" + + x = data["pseudo_step"] + for ax, res in zip(axes, residuals): + ax.semilogy(x, data[res], label=plot_name) + ax.set_ylabel(res) + ax.legend() + ax.grid(which="major", color="gray") + ax.set_xlabel("Pseudo step") + ax.set_yticks(10.0 ** np.arange(-14, -3)) + ax.set_ylim([1e-13, 1e-3]) + ax.set_xlim([0, 10000]) + + +# set output directory +dir_path = os.path.join(os.getcwd(), "figures") +os.makedirs(dir_path, exist_ok=True) + +# initialize figures & axes + +for load in loads: + fig, ax = plt.subplots(figsize=(8, 6)) + figures.append(fig) + axes.append(ax) + +# calculate and plot loads convergence histories +for case_name in case_name_list: + csv_path = os.path.join(os.getcwd(), f"{case_name}", "surface_forces_v2.csv") + data = pd.read_csv(csv_path, skipinitialspace=True) + compute_cl_cd(data) + plot_loads(data, case_name) + +# save loads figures +for i, load in enumerate(loads): + figures[i].savefig(os.path.join(dir_path, load + ".png"), dpi=500) + +for ax in axes: + ax.cla() + +# plot residual convergence histories +for res in residuals: + fig, ax = plt.subplots(figsize=(8, 6)) + figures.append(fig) + axes.append(ax) + +for case_name in case_name_list: + csv_path = os.path.join(os.getcwd(), case_name, "nonlinear_residual_v2.csv") + data = pd.read_csv(csv_path, skipinitialspace=True) + plot_residuals(data, case_name) +for i, res in enumerate(residuals): + figures[i].savefig(os.path.join(dir_path, res + ".png"), dpi=500) diff --git a/examples/Solver_Features/Wall_Model/download_data.py b/examples/Solver_Features/Wall_Model/download_data.py new file mode 100644 index 0000000..e7e25a9 --- /dev/null +++ b/examples/Solver_Features/Wall_Model/download_data.py @@ -0,0 +1,29 @@ +"""Download the results for the wall-resolved and wall-modeled cases""" + +import os + +from flow360 import MyCases + +# read in case_name_list +with open("case_name_list.dat", "r", encoding="utf-8") as file: + case_name_list = file.read().splitlines() + +my_cases = MyCases(limit=None) + +case = None + +for case_name in case_name_list: + case_folder = os.path.join(os.getcwd(), case_name) + os.makedirs(case_folder, exist_ok=True) + # find the latest case with the name corresponding to the name in case_name_list + for case in my_cases: + if case.name == case_name: + break + print(case.name) + # download the files + case.results.download( + nonlinear_residuals=True, + surface_forces=True, + total_forces=True, + destination=case_folder, + ) diff --git a/examples/Solver_Features/Wall_Model/submit_cases_from_downloads.py b/examples/Solver_Features/Wall_Model/submit_cases_from_downloads.py new file mode 100644 index 0000000..2bd0c86 --- /dev/null +++ b/examples/Solver_Features/Wall_Model/submit_cases_from_downloads.py @@ -0,0 +1,64 @@ +"""Case submission through mesh download and upload onto Flexcompute servers""" + +import os.path +from urllib.request import urlretrieve + +import flow360 as fl + +case_name_list = [] + +# download meshes to the current directory + + +URL = "https://simcloud-public-1.s3.amazonaws.com/caseStudies/wallModel/Meshes/Windsor_Wall_Resolved_1e-06.b8.ugrid" +MESH_FILENAME = "Windsor_Wall_Resolved_1e-06.b8.ugrid" + +if not os.path.isfile(MESH_FILENAME): + urlretrieve(URL, MESH_FILENAME) + +URL2 = "https://simcloud-public-1.s3.amazonaws.com/caseStudies/wallModel/Meshes/Windsor_Wall_Model_5e-04.b8.ugrid" +MESH_FILENAME2 = "Windsor_Wall_Model_5e-04.b8.ugrid" + +if not os.path.isfile(MESH_FILENAME2): + urlretrieve(URL2, MESH_FILENAME2) + + +# upload the wall resolved mesh +volume_mesh = fl.VolumeMesh.from_file(MESH_FILENAME, name="Windsor_Wall_Resolved_Mesh") +volume_mesh = volume_mesh.submit() + + +# upload the wall model mesh +volume_mesh2 = fl.VolumeMesh.from_file(MESH_FILENAME2, name="Windsor_Wall_Modeled_Mesh") +volume_mesh2 = volume_mesh2.submit() + +# submit wall-resolved case using json file +params = fl.Flow360Params("Flow360.json") +case = fl.Case.create("Windsor_Wall_Resolved", params, volume_mesh.id) +case_name_list.append(case.name) +case = case.submit() + +# change noSlipWall to wallFunction in params + +Boundaries = params.boundaries +params.boundaries = Boundaries.copy( + update={ + "1": fl.WallFunction(name="Flow.CFDWT.FloorUnder"), + "4": fl.WallFunction(name="Flow.CFDWT.Floor"), + "8": fl.WallFunction(name="Windsor"), + "9": fl.WallFunction(name="Windsor_rear"), + "10": fl.WallFunction(name="Windsor_supports"), + } +) + +# submit wall-modeled case using updated parameters + +case2 = fl.Case.create("Windsor_Wall_Modeled", params, volume_mesh2.id) +case_name_list.append(case2.name) +case2 = case2.submit() + +# dump case names for use in download and post-processing scripts + +with open("case_name_list.dat", "w", encoding="utf-8") as f: + for line in case_name_list: + f.write(f"{line}\n") diff --git a/examples/Solver_Features/Wall_Model/submit_cases_from_id.py b/examples/Solver_Features/Wall_Model/submit_cases_from_id.py new file mode 100644 index 0000000..548a1a5 --- /dev/null +++ b/examples/Solver_Features/Wall_Model/submit_cases_from_id.py @@ -0,0 +1,37 @@ +"""Case submission from mesh ID's present on the Flexcompute servers""" + +import flow360 as fl + +case_name_list = [] + +MESH_ID_WALL_RESOLVED = "2f71bda6-b218-4fb2-9a2b-d79a4d3c5384" +MESH_ID_WALL_MODEL = "4ff4782e-9ad1-4bed-ab2d-419a07cc512b" + + +# submit wall-resolved case using json file +params = fl.Flow360Params("Flow360.json") +case = fl.Case.create("Windsor_Wall_Resolved", params, MESH_ID_WALL_RESOLVED) +case_name_list.append(case.name) +case = case.submit() + +# change noSlipWall to wallFunction in params +Boundaries = params.boundaries +params.boundaries = Boundaries.copy( + update={ + "1": fl.WallFunction(name="Flow.CFDWT.FloorUnder"), + "4": fl.WallFunction(name="Flow.CFDWT.Floor"), + "8": fl.WallFunction(name="Windsor"), + "9": fl.WallFunction(name="Windsor_rear"), + "10": fl.WallFunction(name="Windsor_supports"), + } +) + +# submit wall-modeled case using updated parameters +case2 = fl.Case.create("Windsor_Wall_Modeled", params, MESH_ID_WALL_MODEL) +case_name_list.append(case2.name) +case2 = case2.submit() + +# dump case names for use in download and post-processing scripts +with open("case_name_list.dat", "w", encoding="utf-8") as f: + for line in case_name_list: + f.write(f"{line}\n")