This commit is contained in:
2025-05-13 01:34:53 +03:00
parent 427735e23d
commit 83f3f1c7d4
945 changed files with 633484 additions and 0 deletions
@@ -0,0 +1,35 @@
import os
import matplotlib.pyplot as plt
from control.matlab import *
# Input: Current (A)
# Output: Torque (Nm)
# Params: Kt (Nm/A)
def motor(Kt):
return tf(Kt, 1)
# Mass-Spring-Damper
# Input: Force
# Output: Position
# Params: m (kg)
# b
# k (N/m)
def mass(m, b, k):
A = [[0, 1.], [-k/m, -b/m]]
B = [[0], [1/m]]
C = [[1., 0]]
return ss(A, B, C, 0)
# Input: Torque (Nm)
# Output: Force (N)
# Params: r (m)
def pulley(r):
return tf(r, 1)
sys = series(motor(2.5), pulley(0.015), mass(0.10, 0, 0))
yout, T, xout = step(sys, return_x=True)
print(yout)
# plt.plot(T, yout)
plt.plot(T, xout)
plt.legend(['Displacement', 'Velocity'])
plt.show()
@@ -0,0 +1,52 @@
import numpy as np
import matplotlib.pyplot as plt
encoder_cpr = 2400
stator_slots = 12
pole_pairs = 7
N = data.size
fft = np.fft.rfft(data)
freq = np.fft.rfftfreq(N, d=1./encoder_cpr)
harmonics = [0]
harmonics += [(i+1)*stator_slots for i in range(pole_pairs)]
harmonics += [pole_pairs]
harmonics += [(i+1)*2*pole_pairs for i in range(int(stator_slots/4))]
fft_sparse = fft.copy()
indicies = np.arange(fft_sparse.size)
mask = [i not in harmonics for i in indicies]
fft_sparse[mask] = 0.0
interp_data = np.fft.irfft(fft_sparse)
#%%
#plt.figure()
plt.subplot(3, 1, 1)
plt.plot(data, label='raw')
plt.plot(interp_data, label='selected harmonics IFFT')
plt.title('cogging map')
plt.xlabel('counts')
plt.ylabel('A')
plt.legend(loc='best')
#plt.figure()
plt.subplot(3, 1, 2)
plt.stem(freq, np.abs(fft)/N, label='raw')
plt.stem(freq[harmonics], np.abs(fft_sparse[harmonics])/N, markerfmt='ro', label='selected harmonics')
plt.title('cogging map spectrum')
plt.xlabel('cycles/turn')
plt.ylabel('A')
plt.legend(loc='best')
#plt.figure()
plt.subplot(3, 1, 3)
plt.stem(freq, np.abs(fft)/N, label='raw')
plt.stem(freq[harmonics], np.abs(fft_sparse[harmonics])/N, markerfmt='ro', label='selected harmonics')
plt.title('cogging map spectrum')
plt.xlabel('cycles/turn')
plt.ylabel('A')
plt.legend(loc='best')
@@ -0,0 +1,90 @@
import numpy as np
import sympy as sp
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
do_mass_spring = True
do_PLL = False
bandwidth = 10
pos_ref = 0
vel_ref = 0
init_pos = 1000
init_vel = 0
plotend = 1
plotfrequency = 1000.0
fig, ax1 = plt.subplots()
if do_mass_spring:
# 2nd order system response with manipulation of velocity only
# This is similar to a mass/spring/damper system
# pos_dot = vel
# vel_dot = Kp * delta_pos + Ki * delta_vel
Ki = 2.0 * bandwidth
Kp = 0.25 * Ki**2
def get_Xdot(t, X):
pos = X[0]
vel = X[1]
pos_err = pos_ref - pos
vel_err = vel_ref - vel
pos_dot = vel
vel_dot = Kp * pos_err + Ki * vel_err
Xdot = [pos_dot, vel_dot]
return Xdot
sol = solve_ivp(get_Xdot, (0.0, plotend), [init_pos, init_vel], t_eval=np.linspace(0, plotend, plotend*plotfrequency))
color = 'tab:red'
ax1.set_xlabel('time (s)')
ax1.set_ylabel('pos', color=color)
ax1.plot(np.transpose(sol.t), np.transpose(sol.y[0,:]), label='physical mass', color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:blue'
ax2.set_ylabel('vel', color=color) # we already handled the x-label with ax1
ax2.plot(np.transpose(sol.t), np.transpose(sol.y[1,:]), label='physical mass', color=color)
ax2.tick_params(axis='y', labelcolor=color)
if do_PLL:
# 2nd order system response with a "slipping displacement" term directly on position
# This formulation is given in the sensorless PLL paper
# pos_dot = vel + Kp * delta_pos
# vel_dot = Ki * delta_pos
Kp = 2.0 * bandwidth
Ki = 0.25 * Kp**2
def get_Xdot(t, X):
pos = X[0]
vel = X[1]
pos_err = pos_ref - pos
vel_err = vel_ref - vel
pos_dot = vel + Kp * pos_err
vel_dot = Ki * pos_err
Xdot = [pos_dot, vel_dot]
return Xdot
sol = solve_ivp(get_Xdot, (0.0, plotend), [init_pos, init_vel], t_eval=np.linspace(0, plotend, plotend*plotfrequency))
plt.plot(np.transpose(sol.t), np.transpose(sol.y[0,:]), label='PLL pos')
plt.plot(np.transpose(sol.t), np.transpose(sol.y[1,:]), label='PLL vel')
plt.legend()
plt.show(block=True)
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@@ -0,0 +1,98 @@
% limits
Imax = 64; %A
Umax = 22; %V
Irange = 100; %A. Range for plotting current
omegaMax = 2000; %rad/s mechanical. For plotting voltage ellipses
omegastep = 200; %rad/s mechanical. For plotting voltage ellipses
%%
%350 kv motor
lambda = 2.24/1000;
L = 23e-6;
R = 32e-3;
pp = 7; Poles = pp*2;
Ld = L;
Lq = L;
% %%
% % Donkey
% kv = 820;
% lambda = 60/(kv*2*pi*pp*sqrt(3));
% L = 8e-6; %Guess! TODO: measure
% R = 30e-3; %Guess! TODO: measure
% pp = 7; Poles = pp*2;
% Ld = L;
% Lq = L;
%%
Istep = Irange/400;
Idplt = repmat(-Irange:Istep:Irange,801,1);
Iqplt = repmat((-Irange:Istep:Irange)',1,801);
UmaxSq = (Umax/sqrt(3))^2;
t = linspace(0,2*pi);
IdMaxt = Imax*cos(t);
IqMaxt = Imax*sin(t);
%%
figure(1)
plot(IdMaxt, IqMaxt);
hold on;
%Plot torque
%[c,h] = contour(Idplt,Iqplt, (Poles/2).*(3/2).*(lambda.*Iqplt + (Ld-Lq).*Iqplt.*Idplt), -5:0.25:5);
%clabel(c,h,'LabelSpacing',500);
%Plot voltage ellipses
EllRHS = Ld^2.*(lambda/Ld + Idplt).^2 + Lq^2.*Iqplt.^2;
omegaAtEllipse = sqrt(UmaxSq./EllRHS);
[c,h] = contour(Idplt,Iqplt, omegaAtEllipse./(Poles/2), 0:omegastep:omegaMax);
clabel(c,h,'LabelSpacing',500);
xlabel 'Id (A)'
ylabel 'Iq (A)'
colormap(jet)
c = colorbar;
%c.Label.String = 'Speed (rad/s)';
ylabel(c,'Speed (mechanical rad/s)')
grid on
axis equal
%plot infinite speed point
plot(-lambda/Ld, 0, 'r*');
hold off;
%%
figure(2)
%fw range
t = linspace(pi/2,pi);
IdMaxt = Imax*cos(t);
IqMaxt = Imax*sin(t);
Tmaxt = (Poles/2).*(3/2).*(lambda.*IqMaxt + (Ld-Lq).*IqMaxt.*IdMaxt);
EllRHSmaxt = Ld^2.*(lambda/Ld + IdMaxt).^2 + Lq^2.*IqMaxt.^2;
omegamaxt = sqrt(UmaxSq./EllRHSmaxt);
%MTPA range
MTPAmaxtId = 0; %TODO make work for salient machines
MTPAmaxtIq = Imax;
TmaxtMTPA = (Poles/2).*(3/2).*(lambda.*MTPAmaxtIq + (Ld-Lq).*MTPAmaxtIq.*MTPAmaxtId);
Tmaxt = [repmat(TmaxtMTPA, 1, 100) Tmaxt];
omegamaxt = [linspace(0,omegamaxt(1)) omegamaxt];
%Present mechanical speed
omegamaxt = omegamaxt./(Poles/2);
Pmaxt = Tmaxt.*omegamaxt;
%plotyy(t, Tmaxt, t, omegamaxt);
%plotyy(t, Tmaxt, t, Pmaxt);
%plotyy(t, Pmaxt, t, omegamaxt);
h = plotyy(omegamaxt, Tmaxt, omegamaxt, Pmaxt);
grid on
xlabel 'Speed (mechanical rad/s)'
ylabel 'Torque (Nm)'
ylabel(h(2), 'Power (W)');
@@ -0,0 +1,245 @@
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.optimize import least_squares
from engineering_notation import EngNumber
filename = "oscilloscope.csv"
PLOT_INITAL = True
DO_FITTING = False
PLOT_PROGRESS = False
REPORT_PROGRESS = True
assumed_rotor_resistance = 1
pole_pairs = 2
class ACMotor():
"""
Models an induction motor based on Eq 10 in [1].
[1] https://pdfs.semanticscholar.org/4770/15e472da4c2e05e9ff8c1b921c76a938f786.pdf
Note: This model refers all rotor quantities to the stator, i.e. the
quantities are as if the motor had a winding ratio of k = 1.
"""
# parameters: (name, range)
parameter_definitions = [
('stator_inductance', (0, np.inf), 'H'), # aka l_s, [Henry]
('stator_resistance', (0, np.inf), 'ohm'), # aka r_s, [Ohm]
('rotor_inductance', (0, np.inf), 'H'), # aka l_r [Henry]
# ('rotor_resistance', (0, np.inf), 'ohm'), # aka r_r [Ohm]
('mutual_inductance_factor', (0, 1.0), ''), #[unitless] = l_m**2 / (l_s * l_r)
]
# parameter index lookup
pl = {r[0]:i for i, r in enumerate(parameter_definitions)}
# states: (name, initial_value)
state_definitions = [
('stator_current', 0.0), # aka i_s, [A]
('rotor_flux', 0.0), # aka Phi_r, [Wb]
] # complex numbers
# state index lookup
sl = {r[0]:i for i, r in enumerate(state_definitions)}
def __init__(self, params):
self.params = params
# Assigned in run():
# self.stator_voltage = None
# self.omega_stator = None
# self.omega_rotor = None
def get_mutual_inductance(self):
return np.sqrt(
self.params[ACMotor.pl['mutual_inductance_factor']]
* self.params[ACMotor.pl['stator_inductance']]
* self.params[ACMotor.pl['rotor_inductance']]
)
def system_function(self, t, y):
# local shorthand for params
p = self.params
pl = ACMotor.pl
sl = ACMotor.sl
# rotor_resistance = p[pl['rotor_resistance']]
rotor_resistance = assumed_rotor_resistance
mutual_inductance = self.get_mutual_inductance()
tau_rotor = p[pl['rotor_inductance']] / rotor_resistance # [s]
coupling_factor = mutual_inductance / p[pl['rotor_inductance']] # aka k_r [unitless]
r_sigma = p[pl['stator_resistance']] + coupling_factor**2 * rotor_resistance # [Ohm]
leakage_factor = 1.0 - mutual_inductance**2 / (p[pl['rotor_inductance']] * p[pl['stator_inductance']]) # aka sigma [unitless]
tau_stator_prime = leakage_factor * p[pl['stator_inductance']] / r_sigma # [s]
# [1] Eq 10a
dstator_current_dt = (
-1.0j * self.omega_stator * tau_stator_prime * y[sl['stator_current']]
- coupling_factor / (r_sigma * tau_rotor) * (1.0j*self.omega_rotor * tau_rotor - 1.0) * y[sl['rotor_flux']]
+ 1.0 / r_sigma * self.stator_voltage
- y[sl['stator_current']]
) / tau_stator_prime
# [1] Eq 10b
drotor_flux_dt = (
-1.0j * (self.omega_stator - self.omega_rotor) * tau_rotor * y[sl['rotor_flux']]
+ mutual_inductance * y[sl['stator_current']]
- y[sl['rotor_flux']]
) / tau_rotor
return [dstator_current_dt, drotor_flux_dt]
def run(self, time_series, voltage, omega_stator, omega_rotor):
self.stator_voltage = voltage
self.omega_stator = omega_stator
self.omega_rotor = omega_rotor
y0 = np.array([x[1] for x in ACMotor.state_definitions], dtype=np.complex)
result = solve_ivp(self.system_function, (time_series[0], time_series[-1]), y0, t_eval=time_series)
y = result.y
# compute derived state
rotor_inductance = self.params[ACMotor.pl['rotor_inductance']]
rotor_current = (1/rotor_inductance) * (y[1] - self.get_mutual_inductance() * y[0])
return np.vstack((y, rotor_current))
def print_parameter_info(self):
print()
print('Given parameters:')
print('rotor_resistance = {}ohm'.format(EngNumber(assumed_rotor_resistance)))
print()
print('Fitted parameters:')
for i, r in enumerate(ACMotor.parameter_definitions):
print('{} = {}{}'.format(r[0], EngNumber(self.params[i]), r[2]))
print()
print('Derived parameters:')
mutual_inductance = motor.get_mutual_inductance()
coupling_factor = mutual_inductance / self.params[ACMotor.pl['rotor_inductance']]
torque_constant = pole_pairs * coupling_factor * mutual_inductance
motor_constant = torque_constant / (3.0 * self.params[ACMotor.pl['stator_resistance']])
print('mutual_inductance = {}H'.format(EngNumber(mutual_inductance)))
print('coupling_factor = {}'.format(EngNumber(coupling_factor)))
print('torque_constant = {}Nm/A^2'.format(EngNumber(torque_constant)))
print('motor_constant = {}Nm/W'.format(EngNumber(motor_constant)))
def print_run_info(self, y):
final_stator_current_d = np.real(y[0,-1])
final_stator_current_q = np.imag(y[0,-1])
final_rotor_flux_d = np.real(y[1,-1])
mutual_inductance = self.get_mutual_inductance()
coupling_factor = mutual_inductance / self.params[ACMotor.pl['rotor_inductance']]
final_torque_per_q_amp = pole_pairs * coupling_factor * final_rotor_flux_d
print()
print('Final values:')
print('final_rotor_flux_d = {}Wb'.format(EngNumber(final_rotor_flux_d)))
print('final_stator_current_d = {}A'.format(EngNumber(final_stator_current_d)))
print('final_stator_current_q = {}A'.format(EngNumber(final_stator_current_q)))
print('final_torque_per_q_amp = {}Nm/A'.format(EngNumber(final_torque_per_q_amp)))
def plot_data(t, y, ref, title):
fig, (ax1, ax2) = plt.subplots(2, sharex=True)
ax1b = ax1.twinx()
ax1.plot(t, ref, label='Measured current')
ax1.plot(t, np.real(y[0]), label='Stator current (d)')
ax1.plot(t, np.imag(y[0]), label='Stator current (q)')
ax2.plot(t, np.real(y[2]), label='Rotor current (d)')
ax2.plot(t, np.imag(y[2]), label='Rotor current (q)')
ax1b.plot(t, 1000*np.real(y[1]), 'C3', label='Rotor flux (d)')
ax1b.plot(t, 1000*np.imag(y[1]), 'C4', label='Rotor flux (q)')
ax1.set_xlabel('time [s]')
ax1.set_ylabel('Current [A]')
ax1b.set_ylabel('Flux [mWb]')
ax2.set_ylabel('Current [A]')
plt.title(title)
fig.legend()
plt.show()
# load test data
t = np.arange(4096)/8000.0
voltage_step = 1.0
with open(filename, 'r') as fp:
test_response = np.array([float(x) for x in fp.readlines()])
inital_parameters = np.zeros(len(ACMotor.parameter_definitions))
inital_parameters[ACMotor.pl['stator_inductance']] = 7.72181086e-04
inital_parameters[ACMotor.pl['stator_resistance']] = 3.06884624e-02
inital_parameters[ACMotor.pl['rotor_inductance']] = assumed_rotor_resistance*6.82013522e-02
# inital_parameters[ACMotor.pl['rotor_resistance']] = 1.0e-0
# inital_parameters[ACMotor.pl['mutual_inductance']] = 2.40e-4
inital_parameters[ACMotor.pl['mutual_inductance_factor']] = 8.68671978e-01
# inital_parameters[ACMotor.pl['stator_resistance']] = 1.298
# inital_parameters[ACMotor.pl['stator_inductance']] = 0.157228647
# inital_parameters[ACMotor.pl['rotor_resistance']] = 0.975052932
# inital_parameters[ACMotor.pl['rotor_inductance']] = 0.16674423623999998
# inital_parameters[ACMotor.pl['mutual_inductance']] = 0.157221177
# Plot initial run
if PLOT_INITAL:
print()
print('Initial run:')
motor = ACMotor(inital_parameters)
motor.print_parameter_info()
y = motor.run(
time_series = t,
voltage = voltage_step,
omega_stator = 0,
omega_rotor= 0)
motor.print_run_info(y)
plot_data(t, y, test_response, 'initial')
# Fit to data
def get_residuals(params):
if REPORT_PROGRESS: print(params)
motor = ACMotor(params)
y = motor.run(
time_series = t,
voltage = voltage_step,
omega_stator = 0,
omega_rotor= 0)
residuals = test_response - np.real(y[0])
fitness = sum(residuals**2)
if REPORT_PROGRESS: print(fitness)
if PLOT_PROGRESS:
plot_data(t, y, test_response, 'progress')
return residuals
if DO_FITTING:
print()
print('Fitting parameters:')
optiresult = least_squares(get_residuals, inital_parameters,
bounds=list(zip(*[x[1] for x in ACMotor.parameter_definitions])),
x_scale='jac',
diff_step = 1e-2 * np.array([
7.58192590e-04,
3.07166671e-02,
6.85207075e-02,
# 4.66461518e+00,
8.67540012e-01])
)
print(optiresult.message)
motor = ACMotor(optiresult.x)
motor.print_parameter_info()
y = motor.run(
time_series = t,
voltage = voltage_step,
omega_stator = 0,
omega_rotor= 0)
motor.print_run_info(y)
plot_data(t, y, test_response, 'final')
@@ -0,0 +1,43 @@
%Params
AccelPerA = 3000;
phaseR = 0.033;
Ts = 0.001;
N = 50;
thetaFinal = 200;
x0 = [0;0];
%system definition
%X = [theta; omega]
Ac = [0 1;
0 0];
Bc = [0;
AccelPerA];
C = 0;
D = 0;
SYSC = ss(Ac, Bc, [], []);
SYSD = c2d(SYSC, Ts, 'zoh');
[Phi, Gamma] = predictionmatrices(SYSD.a, SYSD.b, SYSD.c, N);
Df = Gamma(end-1:end,:);
ff = [thetaFinal; 0];
H = 2*eye(N)*(3/2)*phaseR*Ts;
[u, fval] = quadprog(H,[],[],[],Df,ff);
xv = reshape(Gamma*u, 2,N)';
x = xv(:,1);
v = xv(:,2);
kv350_lambda = 2.2e-3;
power = u.*v.*(3/2)*kv350_lambda;
Vbus = 24;
Ib = power/Vbus;
Im = u;
duty = Ib./Im;
CapIsqr = (duty.*(Ib-Im)).^2 + ((1-duty).*Ib).^2;
CapIrms = sqrt(sum(CapIsqr)/N);
CapR = 0.08;
Ncaps = 8;
Cappow = (CapIrms/Ncaps)^2 * CapR
@@ -0,0 +1,14 @@
function [Phi, Gamma, Lambda] = predictionmatrices(A, B, C, N)
%UNTITLED2 Summary of this function goes here
% Detailed explanation goes here
n = size(A,1);
Atilde = [A; zeros((N-1)*n, n)];
k = [zeros(n, N*n); -kron(eye(N-1), A) zeros((N-1)*n,n)] + eye(N*n);
Phi = k\Atilde;
Gamma = k\kron(eye(N), B);
Lambda = kron(eye(N), C);
end
@@ -0,0 +1,9 @@
#%%
from odrive.utils import calculate_thermistor_coeffs
Rload = 3300
R_25 = 10000
Beta = 3434
Tmin = 0
Tmax = 140
calculate_thermistor_coeffs(3, Rload, R_25, Beta, Tmin, Tmax, True)