Skip to content

Assembler API

The Assembler is the primary entry point for building McCode instruments programmatically in Python.

Usage

from mccode_antlr import Flavor
from mccode_antlr.assembler import Assembler

a = Assembler("MyInstrument", flavor=Flavor.MCSTAS)
a.parameter("double E_i = 5.0")

src = a.component("Source", "Source_simple")
src.set_parameter("E0", "E_i")
src.AT([0, 0, 0], "ABSOLUTE")

instr = a.instrument

Reference

mccode_antlr.assembler.Assembler

Interactive instrument assembly

Source code in src/mccode_antlr/assembler/assembler.py
class Assembler:
    """Interactive instrument assembly"""

    def __init__(self, name: str, registries: list[Registry] = None, flavor: Flavor | None = None):
        from ..reader.registry import ordered_registries, ensure_registries
        if flavor is not None:
            if isinstance(flavor, str):
                raise ValueError('flavor must be a Flavor Enum or None')
            registries = ensure_registries(flavor, registries)
        if registries is not None:
            registries = list(ordered_registries(registries))
        self.instrument = Instr(name, source='interactive')
        self.reader = Reader(registries=registries) if registries is not None else Reader()
        self.instrument.registries = self.reader.registries

    @property
    def name(self):
        return self.instrument.name

    def _handle_at_rotate(self, a=None) -> tuple[tuple[Expr, Expr, Expr], Union[Instance,  None]]:
        if a is None:
            return (Expr.float(0), Expr.float(0), Expr.float(0)), None
        if hasattr(a, '__len__') and len(a) == 3:
            a = a, 'ABSOLUTE'
        if not hasattr(a, '__len__') or len(a) != 2:
            raise RuntimeError('Require two-tuple of three values and a reference')
        v, ref = a
        if ref is not None and not isinstance(ref, Instance):
            if 'absolute' == ref.lower():
                ref = None
            elif isinstance(ref, str):
                # Get the component instance by name
                ref = self.instrument.get_component(ref)
            else:
                raise RuntimeError(f'No logic pathway for instance reference {ref}')
        if not hasattr(v, '__len__') or len(v) != 3:
            raise RuntimeError('Position/orientation must have three elements')
        v = tuple(self.instrument.check_expr(x) for x in v)
        return (v[0], v[1], v[2]), ref

    def _handle_at(self, a=None) -> tuple[Vector, Union[Instance, None]]:
        at_tuple, ref = self._handle_at_rotate(a)
        return Vector(*at_tuple), ref

    def _handle_rotate(self, a=None, at_ref=None) -> tuple[Angles, Union[Instance, None]]:
        rot_tuple, ref = self._handle_at_rotate(a)
        return Angles(*rot_tuple), ref or at_ref

    def component(self, name: str, type_name: str, at=None, rotate=None, parameters=None, **kwargs):
        """Add a component to the underlying Instr.

        Parameters
        ----------
        name : str
            The name of the component instance.
        type_name : str
            The name of the component type.
        at : tuple, optional
            The position and orientation of the component instance.
            If not provided, the component will be placed at the origin.
        rotate : tuple, optional
            The rotation of the component instance.
            If not provided, the component will be rotated to match the at argument.
        parameters : dict, optional
            A dictionary of parameter names and values to set for the component instance.
        kwargs : dict, optional
            Properties for the constructed component Instance object. Useful keyword values include
            `when` and `group`.

        Note
        ----
        See `Assembler._handle_at` and `Assembler._handle_rotate` for details on the at and rotate arguments.
        """
        comp_type = self.reader.get_component(type_name)
        if type_name != comp_type.name:
            raise RuntimeError(f"Component resolution failed for {type_name}, found {comp_type.name} instead")
        at, ref = self._handle_at(at)
        instance = Instance(name, comp_type,
                            at_relative=(at, ref), rotate_relative=self._handle_rotate(rotate, ref),
                            **kwargs)
        self.instrument.add_component(instance)
        if isinstance(parameters, dict):
            instance.set_parameters(**parameters)
        return instance

    def parameter(self, par, ignore_repeated=False):
        """Add a parameter to the underlying Instr.

        Note
        ----
        The ignore_repeated keyword argument can be set to True in order to merely ensure that a parameter exists.
        Otherwise, repeatedly specifying the same parameter will raise a RuntimeError.
        """
        if isinstance(par, str):
            par = InstrumentParameter.parse(par)
        if not isinstance(par, InstrumentParameter):
            logger.warning(f'Unhandled parameter {par}')
        self.instrument.add_parameter(par, ignore_repeated=ignore_repeated)

    def parameters(self, *pars, **pairs):
        for par in list(pars):
            if isinstance(par, str):
                par = InstrumentParameter.parse(par)
            if not isinstance(par, InstrumentParameter):
                logger.warning(f'Unhandled parameter(s) {par}')
            self.instrument.add_parameter(par)

        for name, value in pairs.items():
            if not isinstance(value, InstrumentParameter):
                if isinstance(value, dict) and 'unit' in value and 'value' in value:
                    value, unit = value['value'], value['unit']
                elif isinstance(value, (list, tuple)) and len(value) == 2 and isinstance(value[1], str):
                    value, unit = value
                else:
                    unit = ''
                value = InstrumentParameter(name, unit, value if isinstance(value, Expr) else Expr.best(value))
            self.instrument.add_parameter(value)

    def declare(self, string, source=None, line=-1):
        return _rawc_call(self.instrument.DECLARE, string, source, line)

    def declare_array(self, dtype: str, name: str, init: list, source=None, line=-1):
        return self.declare(f'{dtype} {name}[] = {{{",".join(str(x) for x in init)}}};', source=source, line=line)

    def user_vars(self, string, source=None, line=-1):
        return _rawc_call(self.instrument.USERVARS, string, source, line)

    def ensure_user_var(self, string, source=None, line=-1):
        # tying the Assembler to work with C might not be great
        from mccode_antlr.translators.c_listener import extract_c_declared_variables as parse
        variables = parse(string)
        if len(variables) == 0:
            raise ValueError(f'The provided input {string} does not specify a C parameter declaration.')
        if len(variables) != 1:
            print(f'The provided input {string} specifies {len(variables)} C parameter declarations, using only the first')
        decl = variables[0]
        name = decl.name
        dtype = decl.dtype
        for user_vars in self.instrument.user:
            uv_variables = parse(user_vars.source)
            if any(x.dtype == dtype and x.name == name for x in uv_variables):
                return
            if any(x.name == name for x in uv_variables):
                print(f'A USERVARS variable with name {name} but type different than {dtype} has already been defined.')
                return
        return self.user_vars(string, source=source, line=line)

    def initialize(self, string, source=None, line=-1):
        return _rawc_call(self.instrument.INITIALIZE, string, source, line)

    def save(self, string, source=None, line=-1):
        return _rawc_call(self.instrument.SAVE, string, source, line)

    def final(self, string, source=None, line=-1):
        return _rawc_call(self.instrument.FINALLY, string, source, line)

    def metadata(self, name: str, mimetype: str, value: str, source=None):
        from mccode_antlr.common.metadata import MetaData
        self.instrument.add_metadata(MetaData.from_instrument_tokens(source, mimetype, name, value))

component(name, type_name, at=None, rotate=None, parameters=None, **kwargs)

Add a component to the underlying Instr.

Parameters

name : str The name of the component instance. type_name : str The name of the component type. at : tuple, optional The position and orientation of the component instance. If not provided, the component will be placed at the origin. rotate : tuple, optional The rotation of the component instance. If not provided, the component will be rotated to match the at argument. parameters : dict, optional A dictionary of parameter names and values to set for the component instance. kwargs : dict, optional Properties for the constructed component Instance object. Useful keyword values include when and group.

Note

See Assembler._handle_at and Assembler._handle_rotate for details on the at and rotate arguments.

Source code in src/mccode_antlr/assembler/assembler.py
def component(self, name: str, type_name: str, at=None, rotate=None, parameters=None, **kwargs):
    """Add a component to the underlying Instr.

    Parameters
    ----------
    name : str
        The name of the component instance.
    type_name : str
        The name of the component type.
    at : tuple, optional
        The position and orientation of the component instance.
        If not provided, the component will be placed at the origin.
    rotate : tuple, optional
        The rotation of the component instance.
        If not provided, the component will be rotated to match the at argument.
    parameters : dict, optional
        A dictionary of parameter names and values to set for the component instance.
    kwargs : dict, optional
        Properties for the constructed component Instance object. Useful keyword values include
        `when` and `group`.

    Note
    ----
    See `Assembler._handle_at` and `Assembler._handle_rotate` for details on the at and rotate arguments.
    """
    comp_type = self.reader.get_component(type_name)
    if type_name != comp_type.name:
        raise RuntimeError(f"Component resolution failed for {type_name}, found {comp_type.name} instead")
    at, ref = self._handle_at(at)
    instance = Instance(name, comp_type,
                        at_relative=(at, ref), rotate_relative=self._handle_rotate(rotate, ref),
                        **kwargs)
    self.instrument.add_component(instance)
    if isinstance(parameters, dict):
        instance.set_parameters(**parameters)
    return instance

parameter(par, ignore_repeated=False)

Add a parameter to the underlying Instr.

Note

The ignore_repeated keyword argument can be set to True in order to merely ensure that a parameter exists. Otherwise, repeatedly specifying the same parameter will raise a RuntimeError.

Source code in src/mccode_antlr/assembler/assembler.py
def parameter(self, par, ignore_repeated=False):
    """Add a parameter to the underlying Instr.

    Note
    ----
    The ignore_repeated keyword argument can be set to True in order to merely ensure that a parameter exists.
    Otherwise, repeatedly specifying the same parameter will raise a RuntimeError.
    """
    if isinstance(par, str):
        par = InstrumentParameter.parse(par)
    if not isinstance(par, InstrumentParameter):
        logger.warning(f'Unhandled parameter {par}')
    self.instrument.add_parameter(par, ignore_repeated=ignore_repeated)