A permanent-magnet synchronous machine is spinning on a dynamometer at a fixed speed, fed from a two-level six-transistor inverter. Once every switching period you choose which of the eight switching states the inverter holds for that period — one bit per leg, upper device or lower device — and your job is to make the machine's current follow the command it is given while switching as little as you can get away with.
Hard Constraints
- You are called one period at a time and never see a reference before it is commanded. The episode is not handed to you in advance and there is no way to ask for it; the grader holds it and streams it. A controller that tries to plan the whole episode up front has nothing to plan against.
- You are not told the machine's true parameters. The one on the dynamometer has warmed up and aged away from the nameplate, and the resistance, the magnet flux and both inductances are all somewhere else.
- You are not told when the command will next change.
- Exceeding the current limit destroys the inverter: if
|i_dq|ever exceedsi_limthe episode stops there and is charged a flat 5.0. - Budget: 600 s of wall clock per episode, covering
make_controllerand every call toact. That is generous on purpose: there is room to do real work per period, and room to precompute whatever you like before the episode starts. - There is no way to break a rule. A returned value that is not one of the eight switching states is read as state 0, and the machine does what the physics says. Time is the only currency.
What You Have
methods/main/solver.py a working controller -- a real one, not a stub;
read it before you replace it
selfcheck.py a handful of episodes, prints your cost
core.py, model.py the machine model and the episode loop
episodes.py how command trajectories and parameter drift are drawn
operating_points.json the speed / command operating points
python selfcheck.py
python selfcheck.py --solver methods/main/solver.py
selfcheck.py draws its own command trajectories and its own parameter drift
every time you run it, from the same distribution the graded episodes come from.
The number it prints will therefore move a little from run to run. That is the
point: there is no fixed set of episodes to tune against, and a controller that
only works on the ones you happened to see is not worth having.
make_controller is called once, before the episode starts, with a dictionary
describing the setup:
| key | meaning |
|---|---|
u_sup |
DC link voltage, V |
omega |
shaft speed, rad/s, constant for the episode |
periods |
how many switching decisions the episode lasts |
tau |
simulation step, s |
k_sub |
simulation steps per switching period |
i_lim |
current magnitude that destroys the inverter, A |
i_nom |
rated current, A |
w_track, w_sw |
the two weights in the cost below |
nameplate |
p, l_d, l_q, r_s, psi_p — the catalogue machine |
act is then called once per switching period with what a drive can actually
measure at that instant:
| key | meaning |
|---|---|
k |
period index |
i_sd, i_sq |
measured current in the rotor frame, A |
epsilon |
rotor angle, rad |
omega, u_sup |
as above |
i_sd_ref, i_sq_ref |
the current commanded for this period |
last_action |
the state applied in the previous period, or −1 |
The speeds are above the machine's base speed, in field weakening. i_sd_ref
comes from an outer loop that has already chosen how hard to weaken the field: it
is set so the voltage needed to hold the commanded current in the steady state is
between 0.86 and 1.16 times the radius of the largest circle that fits inside the
voltage hexagon. Some commands are therefore comfortably reachable, and some are
not reachable at all — the best you can do there is get as close as the hexagon
allows and stay there.
What You Submit
One function:
def make_controller(spec):
...
return act # act(obs) -> int in 0..7
How It Is Judged
Per episode:
cost = w_track * mean over simulation steps of |i_dq − i_dq_ref| / i_lim
+ w_sw * (total leg transitions) / periods
The first term is charged at every simulation step, not once per switching period, so the ripple inside a period is counted. The second counts how many of the three legs change state from one period to the next — that is what heats the transistors. A destroyed inverter is charged a flat 5.0 and ends the episode.
Your score is the mean cost over the graded episodes, and lower is better.
The graded episodes are drawn from the same distribution selfcheck.py samples
from, but they are not the ones you can see.