mirror of https://github.com/m-labs/artiq.git
565 lines
21 KiB
Python
565 lines
21 KiB
Python
from artiq.language.core import kernel, delay, delay_mu, portable
|
|
from artiq.language.units import us, ns
|
|
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
|
from artiq.coredevice import spi2 as spi
|
|
from artiq.coredevice import urukul, sampler
|
|
|
|
|
|
COEFF_WIDTH = 18
|
|
Y_FULL_SCALE_MU = (1 << (COEFF_WIDTH - 1)) - 1
|
|
COEFF_DEPTH = 10 + 1
|
|
WE = 1 << COEFF_DEPTH + 1
|
|
STATE_SEL = 1 << COEFF_DEPTH
|
|
CONFIG_SEL = 1 << COEFF_DEPTH - 1
|
|
CONFIG_ADDR = CONFIG_SEL | STATE_SEL
|
|
T_CYCLE = (2*(8 + 64) + 2)*8*ns # Must match gateware Servo.t_cycle.
|
|
COEFF_SHIFT = 11
|
|
|
|
|
|
@portable
|
|
def y_mu_to_full_scale(y):
|
|
"""Convert servo Y data from machine units to units of full scale."""
|
|
return y / Y_FULL_SCALE_MU
|
|
|
|
|
|
@portable
|
|
def adc_mu_to_volts(x, gain, corrected_fs=True):
|
|
"""Convert servo ADC data from machine units to Volt."""
|
|
val = (x >> 1) & 0xffff
|
|
mask = 1 << 15
|
|
val = -(val & mask) + (val & ~mask)
|
|
return sampler.adc_mu_to_volt(val, gain, corrected_fs)
|
|
|
|
|
|
class SUServo:
|
|
"""Sampler-Urukul Servo parent and configuration device.
|
|
|
|
Sampler-Urukul Servo is a integrated device controlling one
|
|
8-channel ADC (Sampler) and two 4-channel DDS (Urukuls) with a DSP engine
|
|
connecting the ADC data and the DDS output amplitudes to enable
|
|
feedback. SU Servo can for example be used to implement intensity
|
|
stabilization of laser beams with an amplifier and AOM driven by Urukul
|
|
and a photodetector connected to Sampler.
|
|
|
|
Additionally SU Servo supports multiple preconfigured profiles per channel
|
|
and features like automatic integrator hold.
|
|
|
|
Notes:
|
|
|
|
* See the SU Servo variant of the Kasli target for an example of how to
|
|
connect the gateware and the devices. Sampler and each Urukul need
|
|
two EEM connections.
|
|
* Ensure that both Urukuls are AD9910 variants and have the on-board
|
|
dip switches set to 1100 (first two on, last two off).
|
|
* Refer to the Sampler and Urukul documentation and the SU Servo
|
|
example device database for runtime configuration of the devices
|
|
(PLLs, gains, clock routing etc.)
|
|
|
|
:param channel: RTIO channel number
|
|
:param pgia_device: Name of the Sampler PGIA gain setting SPI bus
|
|
:param cpld_devices: Names of the Urukul CPLD SPI buses
|
|
:param dds_devices: Names of the AD9910 devices
|
|
:param gains: Initial value for PGIA gains shift register
|
|
(default: 0x0000). Knowledge of this state is not transferred
|
|
between experiments.
|
|
:param sampler_hw_rev: Sampler's revision string
|
|
:param core_device: Core device name
|
|
"""
|
|
kernel_invariants = {"channel", "core", "pgia", "cplds", "ddses",
|
|
"ref_period_mu", "corrected_fs"}
|
|
|
|
def __init__(self, dmgr, channel, pgia_device,
|
|
cpld_devices, dds_devices,
|
|
gains=0x0000, sampler_hw_rev="v2.2", core_device="core"):
|
|
|
|
self.core = dmgr.get(core_device)
|
|
self.pgia = dmgr.get(pgia_device)
|
|
self.pgia.update_xfer_duration_mu(div=4, length=16)
|
|
assert len(dds_devices) == len(cpld_devices)
|
|
self.ddses = [dmgr.get(dds) for dds in dds_devices]
|
|
self.cplds = [dmgr.get(cpld) for cpld in cpld_devices]
|
|
self.channel = channel
|
|
self.gains = gains
|
|
self.ref_period_mu = self.core.seconds_to_mu(
|
|
self.core.coarse_ref_period)
|
|
self.corrected_fs = sampler.Sampler.use_corrected_fs(sampler_hw_rev)
|
|
assert self.ref_period_mu == self.core.ref_multiplier
|
|
|
|
@staticmethod
|
|
def get_rtio_channels(channel, **kwargs):
|
|
return [(channel, None)]
|
|
|
|
@kernel
|
|
def init(self):
|
|
"""Initialize the servo, Sampler and both Urukuls.
|
|
|
|
Leaves the servo disabled (see :meth:`set_config`), resets and
|
|
configures all DDS.
|
|
|
|
Urukul initialization is performed blindly as there is no readback from
|
|
the DDS or the CPLDs.
|
|
|
|
This method does not alter the profile configuration memory
|
|
or the channel controls.
|
|
"""
|
|
self.set_config(enable=0)
|
|
delay(3*us) # pipeline flush
|
|
|
|
self.pgia.set_config_mu(
|
|
sampler.SPI_CONFIG | spi.SPI_END,
|
|
16, 4, sampler.SPI_CS_PGIA)
|
|
|
|
for i in range(len(self.cplds)):
|
|
cpld = self.cplds[i]
|
|
dds = self.ddses[i]
|
|
|
|
cpld.init(blind=True)
|
|
prev_cpld_cfg = cpld.cfg_reg
|
|
cpld.cfg_write(prev_cpld_cfg | (0xf << urukul.CFG_MASK_NU))
|
|
dds.init(blind=True)
|
|
cpld.cfg_write(prev_cpld_cfg)
|
|
|
|
@kernel
|
|
def write(self, addr, value):
|
|
"""Write to servo memory.
|
|
|
|
This method advances the timeline by one coarse RTIO cycle.
|
|
|
|
:param addr: Memory location address.
|
|
:param value: Data to be written.
|
|
"""
|
|
addr |= WE
|
|
value &= (1 << COEFF_WIDTH) - 1
|
|
value |= (addr >> 8) << COEFF_WIDTH
|
|
addr = addr & 0xff
|
|
rtio_output((self.channel << 8) | addr, value)
|
|
delay_mu(self.ref_period_mu)
|
|
|
|
@kernel
|
|
def read(self, addr):
|
|
"""Read from servo memory.
|
|
|
|
This method does not advance the timeline but consumes all slack.
|
|
|
|
:param addr: Memory location address.
|
|
"""
|
|
value = (addr >> 8) << COEFF_WIDTH
|
|
addr = addr & 0xff
|
|
rtio_output((self.channel << 8) | addr, value)
|
|
return rtio_input_data(self.channel)
|
|
|
|
@kernel
|
|
def set_config(self, enable):
|
|
"""Set SU Servo configuration.
|
|
|
|
This method advances the timeline by one servo memory access.
|
|
It does not support RTIO event replacement.
|
|
|
|
:param enable (int): Enable servo operation. Enabling starts servo
|
|
iterations beginning with the ADC sampling stage. The first DDS
|
|
update will happen about two servo cycles (~2.3 µs) after enabling
|
|
the servo. The delay is deterministic.
|
|
This also provides a mean for synchronization of servo updates to
|
|
other RTIO activity.
|
|
Disabling takes up to two servo cycles (~2.3 µs) to clear the
|
|
processing pipeline.
|
|
"""
|
|
self.write(CONFIG_ADDR, enable)
|
|
|
|
@kernel
|
|
def get_status(self):
|
|
"""Get current SU Servo status.
|
|
|
|
This method does not advance the timeline but consumes all slack.
|
|
|
|
The ``done`` bit indicates that a SU Servo cycle has completed.
|
|
It is pulsed for one RTIO cycle every SU Servo cycle and asserted
|
|
continuously when the servo is not ``enabled`` and the pipeline has
|
|
drained (the last DDS update is done).
|
|
|
|
This method returns and clears the clip indicator for all channels.
|
|
An asserted clip indicator corresponds to the servo having encountered
|
|
an input signal on an active channel that would have resulted in the
|
|
IIR state exceeding the output range.
|
|
|
|
:return: Status. Bit 0: enabled, bit 1: done,
|
|
bits 8-15: channel clip indicators.
|
|
"""
|
|
return self.read(CONFIG_ADDR)
|
|
|
|
@kernel
|
|
def get_adc_mu(self, adc):
|
|
"""Get the latest ADC reading (IIR filter input X0) in machine units.
|
|
|
|
This method does not advance the timeline but consumes all slack.
|
|
|
|
If reading servo state through this method collides with the servo
|
|
writing that same data, the data can become invalid. To ensure
|
|
consistent and valid data, stop the servo before using this method.
|
|
|
|
:param adc: ADC channel number (0-7)
|
|
:return: 17 bit signed X0
|
|
"""
|
|
# State memory entries are 25 bits. Due to the pre-adder dynamic
|
|
# range, X0/X1/OFFSET are only 24 bits. Finally, the RTIO interface
|
|
# only returns the 18 MSBs (the width of the coefficient memory).
|
|
return self.read(STATE_SEL | (adc << 1) | (1 << 8))
|
|
|
|
@kernel
|
|
def set_pgia_mu(self, channel, gain):
|
|
"""Set instrumentation amplifier gain of a ADC channel.
|
|
|
|
The four gain settings (0, 1, 2, 3) corresponds to gains of
|
|
(1, 10, 100, 1000) respectively.
|
|
|
|
:param channel: Channel index
|
|
:param gain: Gain setting
|
|
"""
|
|
gains = self.gains
|
|
gains &= ~(0b11 << (channel*2))
|
|
gains |= gain << (channel*2)
|
|
self.pgia.write(gains << 16)
|
|
self.gains = gains
|
|
|
|
@kernel
|
|
def get_adc(self, channel):
|
|
"""Get the latest ADC reading (IIR filter input X0).
|
|
|
|
This method does not advance the timeline but consumes all slack.
|
|
|
|
If reading servo state through this method collides with the servo
|
|
writing that same data, the data can become invalid. To ensure
|
|
consistent and valid data, stop the servo before using this method.
|
|
|
|
The PGIA gain setting must be known prior to using this method, either
|
|
by setting the gain (:meth:`set_pgia_mu`) or by supplying it
|
|
(:attr:`gains` or via the constructor/device database).
|
|
|
|
:param adc: ADC channel number (0-7)
|
|
:return: ADC voltage
|
|
"""
|
|
val = self.get_adc_mu(channel)
|
|
gain = (self.gains >> (channel*2)) & 0b11
|
|
return adc_mu_to_volts(val, gain, self.corrected_fs)
|
|
|
|
|
|
class Channel:
|
|
"""Sampler-Urukul Servo channel
|
|
|
|
:param channel: RTIO channel number
|
|
:param servo_device: Name of the parent SUServo device
|
|
"""
|
|
kernel_invariants = {"channel", "core", "servo", "servo_channel"}
|
|
|
|
def __init__(self, dmgr, channel, servo_device):
|
|
self.servo = dmgr.get(servo_device)
|
|
self.core = self.servo.core
|
|
self.channel = channel
|
|
# This assumes the mem channel is right after the control channels
|
|
# Make sure this is always the case in eem.py
|
|
self.servo_channel = (self.channel + 4 * len(self.servo.cplds) -
|
|
self.servo.channel)
|
|
self.dds = self.servo.ddses[self.servo_channel // 4]
|
|
|
|
@staticmethod
|
|
def get_rtio_channels(channel, **kwargs):
|
|
return [(channel, None)]
|
|
|
|
@kernel
|
|
def set(self, en_out, en_iir=0, profile=0):
|
|
"""Operate channel.
|
|
|
|
This method does not advance the timeline. Output RF switch setting
|
|
takes effect immediately and is independent of any other activity
|
|
(profile settings, other channels). The RF switch behaves like
|
|
:class:`artiq.coredevice.ttl.TTLOut`. RTIO event replacement is
|
|
supported. IIR updates take place once the RF switch has been enabled
|
|
for the configured delay and the profile setting has been stable.
|
|
Profile changes take between one and two servo cycles to reach the DDS.
|
|
|
|
:param en_out: RF switch enable
|
|
:param en_iir: IIR updates enable
|
|
:param profile: Active profile (0-31)
|
|
"""
|
|
rtio_output(self.channel << 8,
|
|
en_out | (en_iir << 1) | (profile << 2))
|
|
|
|
@kernel
|
|
def set_dds_mu(self, profile, ftw, offs, pow_=0):
|
|
"""Set profile DDS coefficients in machine units.
|
|
|
|
.. seealso:: :meth:`set_amplitude`
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param ftw: Frequency tuning word (32 bit unsigned)
|
|
:param offs: IIR offset (17 bit signed)
|
|
:param pow_: Phase offset word (16 bit)
|
|
"""
|
|
base = (self.servo_channel << 8) | (profile << 3)
|
|
self.servo.write(base + 0, ftw >> 16)
|
|
self.servo.write(base + 6, (ftw & 0xffff))
|
|
self.set_dds_offset_mu(profile, offs)
|
|
self.servo.write(base + 2, pow_)
|
|
|
|
@kernel
|
|
def set_dds(self, profile, frequency, offset, phase=0.):
|
|
"""Set profile DDS coefficients.
|
|
|
|
This method advances the timeline by four servo memory accesses.
|
|
Profile parameter changes are not synchronized. Activate a different
|
|
profile or stop the servo to ensure synchronous changes.
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param frequency: DDS frequency in Hz
|
|
:param offset: IIR offset (negative setpoint) in units of full scale,
|
|
see :meth:`dds_offset_to_mu`
|
|
:param phase: DDS phase in turns
|
|
"""
|
|
ftw = self.dds.frequency_to_ftw(frequency)
|
|
pow_ = self.dds.turns_to_pow(phase)
|
|
offs = self.dds_offset_to_mu(offset)
|
|
self.set_dds_mu(profile, ftw, offs, pow_)
|
|
|
|
@kernel
|
|
def set_dds_offset_mu(self, profile, offs):
|
|
"""Set only IIR offset in DDS coefficient profile.
|
|
|
|
See :meth:`set_dds_mu` for setting the complete DDS profile.
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param offs: IIR offset (17 bit signed)
|
|
"""
|
|
base = (self.servo_channel << 8) | (profile << 3)
|
|
self.servo.write(base + 4, offs)
|
|
|
|
@kernel
|
|
def set_dds_offset(self, profile, offset):
|
|
"""Set only IIR offset in DDS coefficient profile.
|
|
|
|
See :meth:`set_dds` for setting the complete DDS profile.
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param offset: IIR offset (negative setpoint) in units of full scale
|
|
"""
|
|
self.set_dds_offset_mu(profile, self.dds_offset_to_mu(offset))
|
|
|
|
@portable
|
|
def dds_offset_to_mu(self, offset):
|
|
"""Convert IIR offset (negative setpoint) from units of full scale to
|
|
machine units (see :meth:`set_dds_mu`, :meth:`set_dds_offset_mu`).
|
|
|
|
For positive ADC voltages as setpoints, this should be negative. Due to
|
|
rounding and representation as two's complement, ``offset=1`` can not
|
|
be represented while ``offset=-1`` can.
|
|
"""
|
|
return int(round(offset * (1 << COEFF_WIDTH - 1)))
|
|
|
|
@kernel
|
|
def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0):
|
|
"""Set profile IIR coefficients in machine units.
|
|
|
|
The recurrence relation is (all data signed and MSB aligned):
|
|
|
|
.. math::
|
|
a_0 y_n = a_1 y_{n - 1} + b_0 (x_n + o)/2 + b_1 (x_{n - 1} + o)/2
|
|
|
|
Where:
|
|
|
|
* :math:`y_n` and :math:`y_{n-1}` are the current and previous
|
|
filter outputs, clipped to :math:`[0, 1[`.
|
|
* :math:`x_n` and :math:`x_{n-1}` are the current and previous
|
|
filter inputs in :math:`[-1, 1[`.
|
|
* :math:`o` is the offset
|
|
* :math:`a_0` is the normalization factor :math:`2^{11}`
|
|
* :math:`a_1` is the feedback gain
|
|
* :math:`b_0` and :math:`b_1` are the feedforward gains for the two
|
|
delays
|
|
|
|
.. seealso:: :meth:`set_iir`
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param adc: ADC channel to take IIR input from (0-7)
|
|
:param a1: 18 bit signed A1 coefficient (Y1 coefficient,
|
|
feedback, integrator gain)
|
|
:param b0: 18 bit signed B0 coefficient (recent,
|
|
X0 coefficient, feed forward, proportional gain)
|
|
:param b1: 18 bit signed B1 coefficient (old,
|
|
X1 coefficient, feed forward, proportional gain)
|
|
:param dly: IIR update suppression time. In units of IIR cycles
|
|
(~1.2 µs, 0-255).
|
|
"""
|
|
base = (self.servo_channel << 8) | (profile << 3)
|
|
self.servo.write(base + 3, adc | (dly << 8))
|
|
self.servo.write(base + 1, b1)
|
|
self.servo.write(base + 5, a1)
|
|
self.servo.write(base + 7, b0)
|
|
|
|
@kernel
|
|
def set_iir(self, profile, adc, kp, ki=0., g=0., delay=0.):
|
|
"""Set profile IIR coefficients.
|
|
|
|
This method advances the timeline by four servo memory accesses.
|
|
Profile parameter changes are not synchronized. Activate a different
|
|
profile or stop the servo to ensure synchronous changes.
|
|
|
|
Gains are given in units of output full per scale per input full scale.
|
|
|
|
The transfer function is (up to time discretization and
|
|
coefficient quantization errors):
|
|
|
|
.. math::
|
|
H(s) = k_p + \\frac{k_i}{s + \\frac{k_i}{g}}
|
|
|
|
Where:
|
|
* :math:`s = \\sigma + i\\omega` is the complex frequency
|
|
* :math:`k_p` is the proportional gain
|
|
* :math:`k_i` is the integrator gain
|
|
* :math:`g` is the integrator gain limit
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param adc: ADC channel to take IIR input from (0-7)
|
|
:param kp: Proportional gain (1). This is usually negative (closed
|
|
loop, positive ADC voltage, positive setpoint). When 0, this
|
|
implements a pure I controller.
|
|
:param ki: Integrator gain (rad/s). When 0 (the default)
|
|
this implements a pure P controller. Same sign as ``kp``.
|
|
:param g: Integrator gain limit (1). When 0 (the default) the
|
|
integrator gain limit is infinite. Same sign as ``ki``.
|
|
:param delay: Delay (in seconds, 0-300 µs) before allowing IIR updates
|
|
after invoking :meth:`set`. This is rounded to the nearest number
|
|
of servo cycles (~1.2 µs). Since the RF switch (:meth:`set`) can be
|
|
opened at any time relative to the servo cycle, the first DDS
|
|
update that carries updated IIR data will occur approximately
|
|
between ``delay + 1 cycle`` and ``delay + 2 cycles`` after
|
|
:meth:`set`.
|
|
"""
|
|
B_NORM = 1 << COEFF_SHIFT + 1
|
|
A_NORM = 1 << COEFF_SHIFT
|
|
COEFF_MAX = 1 << COEFF_WIDTH - 1
|
|
|
|
kp *= B_NORM
|
|
if ki == 0.:
|
|
# pure P
|
|
a1 = 0
|
|
b1 = 0
|
|
b0 = int(round(kp))
|
|
else:
|
|
# I or PI
|
|
ki *= B_NORM*T_CYCLE/2.
|
|
if g == 0.:
|
|
c = 1.
|
|
a1 = A_NORM
|
|
else:
|
|
c = 1./(1. + ki/(g*B_NORM))
|
|
a1 = int(round((2.*c - 1.)*A_NORM))
|
|
b0 = int(round(kp + ki*c))
|
|
b1 = int(round(kp + (ki - 2.*kp)*c))
|
|
if b1 == -b0:
|
|
raise ValueError("low integrator gain and/or gain limit")
|
|
|
|
if (b0 >= COEFF_MAX or b0 < -COEFF_MAX or
|
|
b1 >= COEFF_MAX or b1 < -COEFF_MAX):
|
|
raise ValueError("high gains")
|
|
|
|
dly = int(round(delay/T_CYCLE))
|
|
self.set_iir_mu(profile, adc, a1, b0, b1, dly)
|
|
|
|
@kernel
|
|
def get_profile_mu(self, profile, data):
|
|
"""Retrieve profile data.
|
|
|
|
Profile data is returned in the ``data`` argument in machine units
|
|
packed as: ``[ftw >> 16, b1, pow, adc | (delay << 8), offset, a1,
|
|
ftw & 0xffff, b0]``.
|
|
|
|
.. seealso:: The individual fields are described in
|
|
:meth:`set_iir_mu` and :meth:`set_dds_mu`.
|
|
|
|
This method advances the timeline by 32 µs and consumes all slack.
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param data: List of 8 integers to write the profile data into
|
|
"""
|
|
base = (self.servo_channel << 8) | (profile << 3)
|
|
for i in range(len(data)):
|
|
data[i] = self.servo.read(base + i)
|
|
delay(4*us)
|
|
|
|
@kernel
|
|
def get_y_mu(self, profile):
|
|
"""Get a profile's IIR state (filter output, Y0) in machine units.
|
|
|
|
The IIR state is also know as the "integrator", or the DDS amplitude
|
|
scale factor. It is 17 bits wide and unsigned.
|
|
|
|
This method does not advance the timeline but consumes all slack.
|
|
|
|
If reading servo state through this method collides with the servo
|
|
writing that same data, the data can become invalid. To ensure
|
|
consistent and valid data, stop the servo before using this method.
|
|
|
|
:param profile: Profile number (0-31)
|
|
:return: 17 bit unsigned Y0
|
|
"""
|
|
return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile)
|
|
|
|
@kernel
|
|
def get_y(self, profile):
|
|
"""Get a profile's IIR state (filter output, Y0).
|
|
|
|
The IIR state is also know as the "integrator", or the DDS amplitude
|
|
scale factor. It is 17 bits wide and unsigned.
|
|
|
|
This method does not advance the timeline but consumes all slack.
|
|
|
|
If reading servo state through this method collides with the servo
|
|
writing that same data, the data can become invalid. To ensure
|
|
consistent and valid data, stop the servo before using this method.
|
|
|
|
:param profile: Profile number (0-31)
|
|
:return: IIR filter output in Y0 units of full scale
|
|
"""
|
|
return y_mu_to_full_scale(self.get_y_mu(profile))
|
|
|
|
@kernel
|
|
def set_y_mu(self, profile, y):
|
|
"""Set a profile's IIR state (filter output, Y0) in machine units.
|
|
|
|
The IIR state is also know as the "integrator", or the DDS amplitude
|
|
scale factor. It is 17 bits wide and unsigned.
|
|
|
|
This method must not be used when the servo could be writing to the
|
|
same location. Either deactivate the profile, or deactivate IIR
|
|
updates, or disable servo iterations.
|
|
|
|
This method advances the timeline by one servo memory access.
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param y: 17 bit unsigned Y0
|
|
"""
|
|
# State memory is 25 bits wide and signed.
|
|
# Reads interact with the 18 MSBs (coefficient memory width)
|
|
self.servo.write(STATE_SEL | (self.servo_channel << 5) | profile, y)
|
|
|
|
@kernel
|
|
def set_y(self, profile, y):
|
|
"""Set a profile's IIR state (filter output, Y0).
|
|
|
|
The IIR state is also know as the "integrator", or the DDS amplitude
|
|
scale factor. It is 17 bits wide and unsigned.
|
|
|
|
This method must not be used when the servo could be writing to the
|
|
same location. Either deactivate the profile, or deactivate IIR
|
|
updates, or disable servo iterations.
|
|
|
|
This method advances the timeline by one servo memory access.
|
|
|
|
:param profile: Profile number (0-31)
|
|
:param y: IIR state in units of full scale
|
|
"""
|
|
y_mu = int(round(y * Y_FULL_SCALE_MU))
|
|
if y_mu < 0 or y_mu > (1 << 17) - 1:
|
|
raise ValueError("Invalid SUServo y-value!")
|
|
self.set_y_mu(profile, y_mu)
|
|
return y_mu
|