# Copyright (C) 2026 Bence Göblyös
#
# This program is free software: you can redistribute it and/or modify it under
# terms of the GNU General Public License as published by the Free Software
# Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see https://www.gnu.org/licenses/.
"""
This module contains a driver class for Quantum Design instruments, wrapping MultiPyVu.
Examples
--------
Below is a minimum working example showing how to use a PM100D power meter.
.. code-block:: python
from Devices.QD import PPMS
device = PPMS("8fvfvd3.dhcp.nd.edu")
# Go to 77 K at a rate of 2 K/min using the fast settle mode
device.setTemperature(77.0, 2.0, True)
# Wait until the temperature is reached (or a maximum of 4 hours),
# plus another 10 minutes for sample thermalization
device.waitFor(timeout = 4*3600, delay = 600, temp = True)
# Set up a resistance measurement on bridge 1 with a limit of 100 uA, 100 uW and 95 mV,
# and a built-in waiting period of 5 seconds to allow the bridge to configure itself
device.setupBridge(1, channelOn = True, currentLimit = 100, powerLimit = 100, voltageLimit = 95, delay = 5.0):
# Measure resistance
print(f'Sample resistance: {device.getResistance(2)[0]} Ohm')
"""
import MultiPyVu
import numpy as np
import time
import logging
import sys
from IPython.utils.io import capture_output
from Utilities.SetupLogging import fixLogger
_mu0 = 1.25663706127e-6 # in N/A^2
_oeConversion = {
'oe': 1.0,
'oersted': 1.0,
'apm': 1000 / (4*np.pi),
'a/m': 1000 / (4*np.pi),
't': _mu0 * 1000 / (4*np.pi),
'tesla': _mu0 * 1000 / (4*np.pi),
}
[docs]
class PPMS():
"""
Driver class for Quantum Design PPMS systems.
This class implements the client only. If you want to run this
on the same machine MultiVu is running on, make sure to start the
server manually (python -m MultiPyVu) and use 127.0.0.1 as the
address given to the client.
"""
def __init__(self, address):
"""
Initializes the PPMS connection.
Parameters
----------
address : str
The IP address or hostname of the server.
"""
# Set up logger
self.logger = logging.getLogger('instrumpy.PPMS')
self.logger.propagate = True
self.logger.setLevel(logging.NOTSET)
self.logger.debug("Logger initialized.")
self.addr = address
[docs]
def convertMagUnits(self, value, startingUnit, targetUnit):
"""
Magnetic unit conversion.
Parameters
----------
value: float
Numerical value to be converted.
startingUnit: str
Unit in which the value is specified. One of "Oe", "A/m" or "T".
targetUnit: str
Unit to which the value should be converted. One of "Oe", "A/m" or "T".
Returns
-------
converted: float
The converted value.
"""
startingUnit = startingUnit.lower()
targetUnit = targetUnit.lower()
if startingUnit not in _oeConversion:
self.logger.error(f'Starting unit \"{startingUnit}\" is not recognized, result may be NaN. Please use \"Oe\", \"A/m\" or \"T\"')
if targetUnit not in _oeConversion:
self.logger.error(f'Target unit \"{targetUnit}\" is not recognized, result may be NaN. Please use \"Oe\", \"A/m\" or \"T\"')
factor = _oeConversion.get(targetUnit, float('nan')) / _oeConversion.get(startingUnit, float('nan'))
return value * factor
[docs]
def setTemperature(self, setpoint : float, rate = 2.0, fast = False):
"""
Sets the temperature of the cryostat.
Parameters
----------
setpoint: float
Setpoint temperature in Kelvin.
rate: float, default: 2
Approach rate in Kelvin per minute.
fast: bool, default: False
Whether to enable fast settle mode. If false, the no overshoot approach mode will be used.
"""
mode = MultiPyVu.Client.temperature.approach_mode.fast_settle if fast else MultiPyVu.Client.temperature.approach_mode.no_overshoot
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
client.set_temperature(setpoint, rate, mode)
fixLogger()
[docs]
def getTemperature(self):
"""
Reads back cryostat temperature from PPMS.
Returns
-------
temp: float
Current cryostat temperature in Kelvin.
status: str
Status string.
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
temp, status = client.get_temperature()
fixLogger()
return temp, status
[docs]
def getTemperatureSetpoint(self):
"""
Reads back cryostat temperature setpoint from PPMS.
Returns
-------
temp: float
Current cryostat temperature in Kelvin.
rate: float
Cryostat approach rate in Kelvin per minute.
fast: bool
Whether fast settle mode is enabled. False corresponds to the no overshoot method.
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
temp, rate, approach = client.get_temperature_setpoints()
fixLogger()
fast = approach == MultiPyVu.Client.temperature.approach_mode.fast_settle
return temp, rate, fast
[docs]
def setField(self, setpoint, rate, mode = "linear", driven = False, unit = 'T'):
"""
Sets magnetic field inside the PPMS.
Parameters
----------
setpoint: float
Target H-field strength in Oersted (or A/m if si = True).
rate: float
Approach rate in Oersted per second (or A/m/s if si = True).
mode: str, default: "linear"
Approach mode. Possible values are: "linear", "no_overshoot" or "oscillate".
driven: bool, default: False
Sets whether or not the magnet should be in driven mode. False corresponds to persistent mode.
unit: str, default: 'T'
Indicates the units in which the setpoint is given. One of "Oe", "A/m" or "T".
"""
approachMode = getattr(MultiPyVu.Client.field.approach_mode, mode, None)
if approachMode is None:
self.logger.error(f"The entered mode \"{mode}\" is not valid. Please use one of: \"linear\", \"no_overshoot\" or \"oscillate\"")
return
driveMode = MultiPyVu.Client.field.driven_mode.driven if driven else MultiPyVu.Client.field.driven_mode.persistent
setpoint = self.convertMagUnits(setpoint, startingUnit=unit, targetUnit='Oe')
rate = self.convertMagUnits(rate, startingUnit=unit, targetUnit='Oe')
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
client.set_field(setpoint, rate, approachMode, driveMode)
fixLogger()
[docs]
def getField(self, unit = 'T'):
"""
Reads back the magnetic field from the PPMS.
Parameters
----------
unit: str, default: 'T'
Indicates the units in which the response should be given. One of "Oe", "A/m" or "T".
Returns
-------
field: float
Current magnetic field strength in the selected units.
status: str
Status string.
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
field, status = client.get_field()
fixLogger()
field = self.convertMagUnits(field, startingUnit='Oe', targetUnit=unit)
return field, status
[docs]
def getFieldSetpoint(self, unit = 'T'):
"""
Reads back magnetic field setpoint from PPMS.
Parameters
----------
unit: str, default: 'T'
Indicates the units in which the response should be given. One of "Oe", "A/m" or "T".
Returns
-------
setpoint: float
Target H-field strength in Oersted (or A/m if si = True).
rate: float
Approach rate in Oersted per second (or A/m/s if si = True).
mode: str, default: "linear"
Approach mode. Possible values are: "linear", "no_overshoot" or "oscillate".
driven: bool, default: False
Whether or not the magnet is in driven mode. False corresponds to persistent mode.
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
field, rate, approach, driven = client.get_field_setpoints()
fixLogger()
field = self.convertMagUnits(field, startingUnit='Oe', targetUnit=unit)
rate = self.convertMagUnits(rate, startingUnit='Oe', targetUnit=unit)
driven = driven == "driven"
return field, rate, approach, driven
[docs]
def setChamber(self, state):
"""
Sets the PPMS chamber state.
Parameters
----------
state: str
Chamber state. Possible values are: "seal", "purge_seal", "vent_seal", "pump_continuous", "vent_continous" or "high_vacuum".
"""
mode = getattr(MultiPyVu.Client.chamber.mode, state, None)
if mode is None:
self.logger.error(f"The entered state \"{mode}\" is not valid. Please use one of: \"seal\", \"purge_seal\", \"vent_seal\", \"pump_continuous\", \"vent_continous\" or \"high_vacuum\"")
return
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
client.set_chamber(mode)
fixLogger()
[docs]
def getChamber(self):
"""
Gets the PPMS chamber state.
Returns
-------
state: str
Chamber state. Note: not the same values that are used for setChamber().
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
resp = client.get_chamber()
fixLogger()
return resp
[docs]
def waitFor(self, timeout = 120, delay = 0, temp = False, field = False, chamber = False):
"""
Wait for setpoints to be reached.
Parameters
----------
timeout: int, default: 120
Time in seconds after which the command will return even if the setpoit is not reached.
delay: int, default: 0
Time in seconds to wait after all setpoits have been reached. Useful for letting the sample thermalize.
temp: bool, default: False
Whether or not to wait for temperature setpoint.
field: bool, default: False
Whether or not to wait for H-field setpoint.
chamber: bool, default: False
Whether or not to wait for the chamber to complete its current operation.
Returns
-------
result: bool
Whether or not the setpoit was reached before the timeout.
"""
if not (temp or field or chamber):
self.logger.warning("At least one of temp, field or chamber must be enabled.")
return False
tempWait = MultiPyVu.Client.temperature.waitfor if temp else 0
fieldWait = MultiPyVu.Client.field.waitfor if field else 0
chamberWait = MultiPyVu.Client.chamber.waitfor if chamber else 0
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
client.wait_for(delay, timeout, tempWait | fieldWait | chamberWait)
fixLogger()
return self.isSteady(temp = temp, field = field, chamber = chamber)
[docs]
def isSteady(self, temp = False, field = False, chamber = False):
"""
Check whether the setpoits have been reached.
Parameters
----------
temp: bool, default: False
Whether or not to check for if temperature is in a steady state.
field: bool, default: False
Whether or not to check if H-field setpoint has been reached.
chamber: bool, default: False
Whether or not to check if the chamber has completes its last operation.
Returns
-------
result: bool
Whether or not the system is in a steady state.
"""
if not (temp or field or chamber):
self.logger.warning("At least one of temp, field or chamber must be enabled.")
return False
tempWait = MultiPyVu.Client.temperature.waitfor if temp else 0
fieldWait = MultiPyVu.Client.field.waitfor if field else 0
chamberWait = MultiPyVu.Client.chamber.waitfor if chamber else 0
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
res = client.is_steady(tempWait | fieldWait | chamberWait)
fixLogger()
if res is not None:
return res
else:
return False
[docs]
def setupBridge(self, bridge, channelOn, currentLimit, powerLimit, voltageLimit, delay = 5.0):
"""
Set up bridge for resistivity measurement
Parameters
----------
bridge: int
Number of the target bridge.
channelOn: bool
Whether or not to enable the selected bridge.
currentLimit: float
Current limit in uA.
powerLimit: float
Power limit in uW.
voltageLimit: float
Voltage limit in mV.
delay: float, default: 5.0
Number of seconds to wait after sending the configuration.
This allows the module to configure itself before a measurement is taken.
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
client.resistivity.bridge_setup(
bridge_number = bridge,
channel_on = channelOn,
current_limit_uA = currentLimit,
power_limit_uW = powerLimit,
voltage_limit_mV = voltageLimit
)
fixLogger()
time.sleep(delay)
[docs]
def setCC(self, bridge, current):
"""
Set up bridge to output a constant current.
Parameters
----------
bridge: int
Number of the target bridge.
current: float
Constant current in uA.
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
client.resistivity.set_current(bridge_number, current)
fixLogger()
[docs]
def getResistance(self, bridge : int):
"""
Measure resistance with the given bridge.
Parameters
----------
bridge: int
Number of the target bridge.
Returns
-------
res: float
Measured resistance, presumably in Ohms.
"""
res = None
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
res = client.resistivity.get_resistance(bridge)
fixLogger()
return res
[docs]
def getCurrent(self, bridge : int):
"""
Measure current on the given bridge.
Parameters
----------
bridge: int
Number of the target bridge.
Returns
-------
current: float
Measured current, presumably in uA.
"""
res = None
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
res = client.resistivity.get_current(bridge)
fixLogger()
return res
[docs]
def setRotatorPosition(self, position : float, rate = 100.0):
"""
Set the horizontal rotator's position.
Parameters
----------
position: float
Rotator position in degrees.
rate: float, default: 100
Roation speed in degrees per second.
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
client.set_position(position, rate)
fixLogger()
[docs]
def getRotatorPosition(self):
"""
Reads back rotator position from PPMS.
Returns
-------
position: float
Current rotator position in degrees.
status: str
Status string.
"""
with capture_output():
with MultiPyVu.Client(host=self.addr) as client:
res = client.get_position()
fixLogger()
return res