SITCOMTN-092
M1M3 Force Balance System - Inertia Compensation#
Abstract
The M1M3 Force Balance System was engineered to mitigate the influence of gravity/elevation forces, thermal fluctuations, and inertia impacts. This technical note presents an initial analysis of the Force Balance System’s performance when implementing corrections to account for inertia effects.
Note
This technote is a work-in-progress.
Abstract#
The M1M3 Force Balance System was engineered to mitigate the influence of gravity/elevation forces, thermal fluctuations, and inertia impacts. This technical note presents an initial analysis of the Force Balance System’s performance when implementing corrections to account for inertia effects.
M1M3 actuator movies#
Craig Lage - 20-Apr-23 The 17 tons of mirror are supported by 156 pneumatic actuators where 44 are single-axis and provide support only on the axial direction, 100 are dual-axis providing support in the axial and lateral direction, and 12 are dual-axis providing support in the axial and cross lateral directions. Positioning is provided by 6 hard points in a hexapod configuration which moves the mirror to a fixed operational position that shall be maintained during telescope operations. The remaining optical elements will be moved relative to this position in order to align the telescope optics. Support and optical figure correction is provided by 112 dual axis and 44 single axis pneumatic actuators.
Ticket SITCOM-763#
M1M3 should be raised for this test. (Check this tbc)
We tested the M1M3 force balance system by applying external force over the surrogate. The force was applied by stepping on random position on the surrogate surface.
Date: 18.04.23 15.30 - 15.40h CLT.
We looked at the M1M3 EUI at the measured forces at the Actuator 2D map.
The expected result was that we see smooth gradients depending on where people were stepping.
We see the movement in the applied actuator forces. See attached video.
For detailed offline analysis: Create an animation of the 2D map for this period with a fixed color scale. Use the nominal position as a reference position.
Prepare the notebook#
python # Directory to store the data
% dir_name = "/home/c/cslage/u/MTM1M3/movies/"
%
% # Times to make the plot
% start = "2023-04-18 16:10:00Z"
% end = "2023-04-18 16:15:00Z"
%
% autoScale = True
% # The following are only used if autoScale = False
% zmin = 0.0 # In nt
% zmax = 2000.0 # In nt
% lateral_max = 1500.0 # In nt
%
% # The following average the first 100 data points
% # and subtract these from the measurements
% # If subtractBasline = False, the unmodified values will be plotted
% subtract_baseline = True
% baseline_t0 = 0.0
% baseline_t1 = 100.0
%
% # The following allows you to plot only every nth data point
% # If this value is 1, a frame will be made for every data point
% # Of course, this takes longer
% # If this value is 50, it will make a frame every second
% frame_n = 50
python import asyncio import glob import os import shlex import subprocess import sys from pathlib import Path from datetime import datetime import matplotlib.pyplot as plt
import numpy as np
from astropy.time import Time, TimeDelta
from matplotlib.colors import LightSource
from lsst.ts.xml.tables.m1m3 import FAOrientation, FATable, FAType
from lsst_efd_client import EfdClient
Set up the necessary subroutines#
def actuator_layout(ax):
""" Plot a visualization of the actuator locations and types Parameters ---------- ax : a matplotlib.axes object
Returns
-------
No return, only the ax object which was input
"""
ax.set_xlabel("X position (m)")
ax.set_ylabel("Y position (m)")
ax.set_title("M1M3 Actuator positions and type\nHardpoints are approximate", fontsize=18)
types = [
[FAType.SAA, FAOrientation.NA, 'o', 'Z', 'b'],
[FAType.DAA, FAOrientation.Y_PLUS, '^', '+Y','g'],
[FAType.DAA, FAOrientation.Y_MINUS, 'v', '-Y', 'cyan'],
[FAType.DAA, FAOrientation.X_PLUS, '>', '+X', 'r'],
[FAType.DAA, FAOrientation.X_MINUS, '<', '-X', 'r'],
]
for [type, orient, marker, label, color] in types:
xs = []
ys = []
for fa in FATable:
x = fa.x_position
y = fa.y_position
if fa.actuator_type == type and \
fa.orientation == orient:
xs.append(x)
ys.append(y)
else:
continue
ax.scatter(xs, ys, marker=marker, color=color, s=200, label=label)
# Now plot approximate hardpoint location
Rhp = 3.1 # Radius in meters
for i in range(6):
theta = 2.0 * np.pi / 6.0 * float(i)
if i == 0:
ax.scatter(Rhp * np.cos(theta), Rhp * np.sin(theta), marker='o', color='magenta', \
s=200, label='HP')
else:
ax.scatter(Rhp * np.cos(theta), Rhp * np.sin(theta), marker='o', color='magenta', \
s=200, label='_nolegend_')
ax.legend(loc='lower left', fontsize=9)
return
def bar_chart_z(df, df_zero, ax, index, zmin, zmax):
""" Plot a 3D bar chart of the actuator Z forces
Parameters
----------
df: pandas dataframe
The pandas dataframe object with the force actuator data
df_zero: pandas dataframe
The pandas dataframe object of the quiescent force data
which will be subtracted off from the force actuator data
ax : a matplotlib.axes object
index: 'int'
The index of the movie frame
zmin: 'float'
The minimum force value for the plot
zmax: 'float'
The maximum force value for the plot
Returns
-------
No return, only the ax object which was input
"""
ax.set_xlabel("X position (m)")
ax.set_ylabel("Y position (m)")
ax.set_zlabel("Force (nt)")
ax.set_title("M1M3 Actuator Z forces", fontsize=18)
light_source = LightSource(azdeg=180, altdeg=78)
grey_color = '0.9'
colors = []
xs = []
ys = []
for fa in FATable:
x = fa.x_position
y = fa.y_position
xs.append(x)
ys.append(y)
if fa.actuator_type == FAType.SAA:
colors.append('blue'); colors.append('blue')
colors.append(grey_color); colors.append(grey_color)
colors.append(grey_color); colors.append(grey_color)
else:
if fa.orientation in [FAOrientation.Y_PLUS, FAOrientation.Y_MINUS]:
colors.append('green'); colors.append('green')
colors.append(grey_color); colors.append(grey_color)
colors.append(grey_color); colors.append(grey_color)
else:
colors.append('red'); colors.append('red')
colors.append(grey_color); colors.append(grey_color)
colors.append(grey_color); colors.append(grey_color)
zs = np.zeros([len(FATable)])
for fa in FATable:
name=f"zForce{fa.index}"
zs[fa.index] = df.iloc[index][name] - df_zero.iloc[0][name]
dxs = 0.2 * np.ones([len(FATable)])
dys = 0.2 * np.ones([len(FATable)])
bottom = np.zeros([len(FATable)])
ax.bar3d(xs, ys, bottom, dxs, dys, zs, shade=True, alpha=0.5, \
lightsource=light_source, color=colors)
ax.set_zlim(zmin, zmax)
ax.view_init(elev=30., azim=225)
return
def heat_map_z(df, df_zero, ax, index, zmin, zmax):
""" Plot a "heat map" of the actuator Z forces
Parameters
----------
df: pandas dataframe
The pandas dataframe object with the force actuator data
df_zero: pandas dataframe
The pandas dataframe object of the quiescent force data
which will be subtracted off from the force actuator data
ax : a matplotlib.axes object
index: 'int'
The index of the movie frame
zmin: 'float'
The minimum force value for the plot
zmax: 'float'
The maximum force value for the plot
Returns
-------
No return, only the ax object which was input
"""
ax.set_xlabel("X position (m)")
ax.set_ylabel("Y position (m)")
ax.set_title("M1M3 Actuator Z forces (nt)", fontsize=18)
types = [
[FAType.SAA, FAOrientation.NA, 'o', 'Z'],
[FAType.DAA, FAOrientation.Y_PLUS, '^', '+Y'],
[FAType.DAA, FAOrientation.Y_MINUS, 'v', '-Y'],
[FAType.DAA, FAOrientation.X_PLUS, '>', '+X'],
[FAType.DAA, FAOrientation.X_MINUS, '<', '-X'],
]
for [type, orient, marker, label] in types:
xs = []
ys = []
zs = []
for fa in FATable:
x = fa.x_position
y = fa.y_position
if fa.actuator_type == type and \
fa.orientation == orient:
xs.append(x)
ys.append(y)
name=f"zForce{fa.index}"
zs.append(df.iloc[index][name] - df_zero.iloc[0][name])
im = ax.scatter(xs, ys, marker=marker, c=zs, cmap='RdBu_r', \
vmin=zmin, vmax=zmax, s=50, label=label)
plt.colorbar(im, ax=ax,fraction=0.055, pad=0.02, cmap='RdBu_r')
return
def lateral_forces(df, df_zero, ax, index, force_max):
""" Plot a 2D whisker plot of the actuator X and Y forces
Parameters
----------
df: pandas dataframe
The pandas dataframe object with the force actuator data
df_zero: pandas dataframe
The pandas dataframe object of the quiescent force data
which will be subtracted off from the force actuator data
ax : a matplotlib.axes object
index: 'int'
The index of the movie frame
force_max: 'float'
maximum force values for scaling the whisker arrows
Returns
-------
No return, only the ax object which was input
"""
ax.set_xlabel("X position (m)")
ax.set_ylabel("Y position (m)")
ax.set_title("M1M3 lateral forces (nt)", fontsize=18)
ax.set_xlim(-4.5,4.5)
ax.set_ylim(-4.5,4.5)
types = [
[FAType.DAA, FAOrientation.Y_PLUS, '^', '+Y','g'],
[FAType.DAA, FAOrientation.Y_MINUS, 'v', '-Y', 'cyan'],
[FAType.DAA, FAOrientation.X_PLUS, '>', '+X', 'r'],
[FAType.DAA, FAOrientation.X_MINUS, '<', '-X', 'r'],
]
for [type, orient, marker, label, color] in types:
xs = []
ys = []
arrow_xs = []
arrow_ys = []
for fa in FATable:
x = fa.x_position
y = fa.y_position
if fa.actuator_type == type and \
fa.orientation == orient:
xs.append(x)
ys.append(y)
if orient == FAOrientation.X_PLUS:
name = f"xForce{fa.x_index}"
arrow_xs.append(df.iloc[index][name] / force_max)
arrow_ys.append(0.0)
elif orient == FAOrientation.X_MINUS:
name = f"xForce{fa.x_index}"
arrow_xs.append(-df.iloc[index][name] / force_max)
arrow_ys.append(0.0)
elif orient == FAOrientation.Y_PLUS:
name = f"yForce{fa.y_index}"
arrow_xs.append(0.0)
arrow_ys.append(df.iloc[index][name] / force_max)
else:
name = f"yForce{fa.y_index}"
arrow_xs.append(0.0)
arrow_ys.append(-df.iloc[index][name] / force_max)
else:
continue
ax.scatter(xs, ys, marker=marker, color=color, s=50, label=label)
for ii in range(len(xs)):
ax.arrow(xs[ii], ys[ii], arrow_xs[ii], arrow_ys[ii], color=color)
ax.plot([-4.0,-3.0], [-4.0,-4.0], color='g')
ax.text(-4.0, -4.3, f"{force_max} nt")
return
def get_zero_values_and_limits(df, subtract_baseline, t0, t1):
""" Plot a 2D whisker plot of the actuator X and Y forces
Parameters
----------
df: pandas dataframe
The pandas dataframe object with the force actuator data.
subtract_baseline : 'bool'
Determines whether or not to subtract off a baseline value from the plots.
t0: 'float'
The time from the beginning of the dataframe when the baseline
quiescent period (which will be subtracted off) begins.
t1: 'float'
The time from the beginning of the dataframe when the baseline
quiescent period (which will be subtracted off) ends.
Returns
-------
zmin: 'float'
The minimum force value for the Z force plots
zmax: 'float'
The maximum force value for the Z force plots
lateral_max: 'float'
The maximum force value for the X,Y whisker plots
df_zero: pandas dataframe
The pandas dataframe object of the quiescent force data
which will be subtracted off from the force actuator data
"""
df_zero = df.head(1)
for column_name in df_zero.columns:
try:
if subtract_baseline:
df_zero.iloc[0, df_zero.columns.get_loc(column_name)] = \
np.median(df[column_name].values[t0:t1])
else:
df_zero.iloc[0, df_zero.columns.get_loc(column_name)] = 0.0
except:
continue
# Now calculate the limits
zmin = 0.0; ymin = 0.0; xmin = 0.0; zmax = 0.0; ymax = 0.0; xmax = 0.0
for fa in FATable:
name = f"zForce{fa.z_index}"
zmin = min(zmin, np.min(df[name] - df_zero.iloc[0][name]))
zmax = max(zmax, np.max(df[name] - df_zero.iloc[0][name]))
if fa.y_index is not None:
name = f"yForce{fa.y_index}"
ymin = min(ymin, np.min(df[name] - df_zero.iloc[0][name]))
ymax = max(ymax, np.max(df[name] - df_zero.iloc[0][name]))
if fa.x_index is not None:
name = f"xForce{fa.x_index}"
xmin = min(xmin, np.min(df[name] - df_zero.iloc[0][name]))
xmax = max(xmax, np.max(df[name] - df_zero.iloc[0][name]))
lateral_max = max(xmax, ymax, -xmin, -ymin)
return [round(zmin), round(zmax), round(lateral_max), df_zero]
Now generate the frames#
This will take some time
client = EfdClient('usdf_efd')
forces = await client.select_time_series("lsst.sal.MTM1M3.forceActuatorData", "*",
Time(start, scale='utc'), Time(end, scale='utc'))
timestamp = forces.index[0].isoformat().split('.')[0].replace('-','').replace(':','')
os.makedirs(Path(dir_name) / f"movie_{timestamp}", exist_ok=True)
[auto_zmin, auto_zmax, auto_lateral_max, forces_zero] = \
get_zero_values_and_limits(forces, subtract_baseline, baseline_t0, baseline_t1)
if autoScale:
zmin = auto_zmin
zmax = auto_zmax
lateral_max = auto_lateral_max
# Build the individual frames
fig = plt.figure(figsize=(16,16))
for n in range(0, len(forces), frame_n):
ax1 = fig.add_subplot(2,2,1)
actuator_layout(ax1)
ax2 = fig.add_subplot(2,2,2, projection='3d')
bar_chart_z(forces, forces_zero, ax2, n, zmin, zmax)
ax3 = fig.add_subplot(2,2,3)
lateral_forces(forces, forces_zero, ax3, n, lateral_max)
ax4 = fig.add_subplot(2,2,4)
heat_map_z(forces, forces_zero, ax4, n, zmin, zmax)
plt.savefig(f"{dir_name}/movie_{timestamp}/Frame_{n:05d}.png")
plt.clf()
len(forces)
Now build the movie#
print(f"\033[1mThe movie name will be: {dir_name}movie_{timestamp}/m1m3_movie.mp4\033[0m")
command = f"ffmpeg -pattern_type glob -i '{dir_name}movie_{timestamp}/*.png' -f mp4 -vcodec libx264 -pix_fmt yuv420p -framerate 50 -y {dir_name}movie_{timestamp}/m1m3_movie.mp4" args = shlex.split(command) build_movie = subprocess.Popen(args) build_movie.wait()
See the reStructuredText Style Guide to learn how to create sections, links, images, tables, equations, and more.