Grouped Parameters
This example is to demonstrate syntax and flexibility of using parameter groups along with the Numpy mathematic library for advanced parameterization.
The Problem
Consider the below setup. There is an airfoil, for which I want to apply a force load on the end. In the initial setup, the force load is in the negative Y axis direction. However, I want to sweep the angle of this vectory in the XY plane from -45˚ to +45˚ from it’s current vertical position over 20 simulations.
Initial Code
import onscale as on
with on.Simulation('Airfoil-Stress') as sim:
geometry = on.CadFile('Airfoil.step')
materials = on.CloudMaterials('onscale')
aluminum = materials['Aluminum']
aluminum >> geometry.parts[0]
force = on.loads.Force(1000, [0, -1, 0])
force >> geometry.parts[0].faces[1]
restraint = on.loads.Restraint(x=True, y=True, z=True)
restraint >> geometry.parts[0].faces[0]
on.fields.Displacement()
on.fields.Stress()
on.fields.Strain()
on.fields.VonMises()
probe = on.probes.ResultantForce(geometry.parts[0].faces[0])
The Solution
The issue with defining this sweep is that two values need to be parametrized simultaneously, both the x
and y
component of the on.loads.Force
vector.
import onscale as on
with on.Simulation('Airfoil-Stress') as sim:
param_x = # ???
param_y = # ???
force = on.loads.Force(1000, [param_x, param_y, 0])
We can start by defining our \(`x`\) and \(`y`\) components in terms of our sweep angle \(`\theta`\) using Numpy:
import numpy as np
min_theta = -np.pi / 4
max_theta = np.pi / 4
theta = np.linspace(min_theta, max_theta, 20)
x = -np.sin(theta)
y = -np.cos(theta)
print("X:", x)
print("Y:", y)
X: [ 0.70710678 0.64629924 0.58107682 0.51188505 0.43919659 0.36350797
0.28533622 0.20521534 0.12369263 0.04132497 -0.04132497 -0.12369263
-0.20521534 -0.28533622 -0.36350797 -0.43919659 -0.51188505 -0.58107682
-0.64629924 -0.70710678]
Y: [-0.70710678 -0.76308407 -0.81384872 -0.85905395 -0.89839098 -0.93159109
-0.95842748 -0.97871685 -0.99232058 -0.99914576 -0.99914576 -0.99232058
-0.97871685 -0.95842748 -0.93159109 -0.89839098 -0.85905395 -0.81384872
-0.76308407 -0.70710678]
Explanation:
Define our minimum angle -45˚ or \(`-\frac{\pi}{4}`\)
Define our maximum angle 45˚ or \(`\frac{\pi}{4}`\)
Create a linear sweep of 20 points between these two angles \(`\theta`\)
Recover the \(`x`\) and \(`y`\) components using trig functions
Note: The trig functions are negative since our vectory is in the \(`-y`\) direction, and we use \(`\cos(y)`\) instead of \(`\sin(y)`\) since our 0˚ corresponds to the \(`y`\) unit vector.
To verify our sweep is correct, let’s plot the vectors using Matplotlib:
import matplotlib.pyplot as plt
plt.quiver(x, y, scale=10)
That’s exactly what we expected. Now to use the x
and y
arrays we generated as parameter sweeps, we can use the onscale.parametrize
function to automatically generate grouped parameters from our arrays:
import numpy as np
import onscale as on
min_theta = -np.pi / 4
max_theta = np.pi / 4
theta = np.linspace(min_theta, max_theta, 20)
x = -np.sin(theta)
y = -np.cos(theta)
with on.Simulation('Airfoil-Stress') as sim:
param_x, param_y = on.parametrize(x, y)
force = on.loads.Force(1000, [param_x, param_y, 0])
And that’s it! The on.parametrize
turns our arrays into parameters that are grouped, meaning that our simulation sweep with iterate over them together (instead of taking all possible combinations of x
and y
). We will now get a 20-simulation sweep of our desired force loads.
Final Code
import numpy as np
import onscale as on
min_theta = -np.pi / 4
max_theta = np.pi / 4
theta = np.linspace(min_theta, max_theta, 20)
x = -np.sin(theta)
y = -np.cos(theta)
with on.Simulation('Airfoil-Stress') as sim:
param_x, param_y = on.parametrize(x, y)
geometry = on.CadFile('Airfoil.step')
materials = on.CloudMaterials('onscale')
aluminum = materials['Aluminum']
aluminum >> geometry.parts[0]
force = on.loads.Force(1000, [param_x, param_y, 0])
force >> geometry.parts[0].faces[1]
restraint = on.loads.Restraint(x=True, y=True, z=True)
restraint >> geometry.parts[0].faces[0]
on.fields.Displacement()
on.fields.Stress()
on.fields.Strain()
on.fields.VonMises()
probe = on.probes.ResultantForce(geometry.parts[0].faces[0])
Under the Hood
The on.parametrize
is a convenience method for turning arrays into grouped parameters. In the example we used just 2 input arrays, but the function actually accepts an arbitrary number of arrays:
import numpy as np
import onscale as on
x = np.linspace(0, 2 * np.pi, 100)
y = np.cos(x)
z = np.sin(x)
with on.Simulation('demo') as sim:
px, py, pz = on.parametrize(x, y, z)
As long as all arrays passed to on.parametrize
are the same length, we get back one parameter for each array. The above code will expand to:
import numpy as np
import onscale as on
x = np.linspace(0, 2 * np.pi, 100)
y = np.cos(x)
z = np.sin(x)
with on.Simulation('demo') as sim:
px = on.CustomParameter('custom_1', x)
py = on.CustomParameter('custom_2', y)
pz = on.CustomParameter('custom_3', z)
group = on.ParameterGroup()
px >> group
py >> group
pz >> group
Any array of values can be made into a simulation parameter with CustomParameter
. By default, these three each have 100 values, which would result in \(`100^3 = 1000000`\) simulations of all possible combinations. If we create a ParameterGroup
and group them together, we instead will iterate over the zipped 100 values together.