moreniius 0.6.3__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {moreniius-0.6.3/src/moreniius.egg-info → moreniius-0.8.0}/PKG-INFO +3 -3
  2. {moreniius-0.6.3 → moreniius-0.8.0}/pyproject.toml +2 -2
  3. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/mccode/instance.py +4 -1
  4. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/mccode/instr.py +57 -24
  5. moreniius-0.8.0/src/moreniius/mccode/mccode.py +154 -0
  6. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/mccode/orientation.py +39 -14
  7. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/utils.py +25 -1
  8. {moreniius-0.6.3 → moreniius-0.8.0/src/moreniius.egg-info}/PKG-INFO +3 -3
  9. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius.egg-info/SOURCES.txt +2 -0
  10. moreniius-0.8.0/src/moreniius.egg-info/requires.txt +5 -0
  11. moreniius-0.8.0/tests/bifrost.instr.json +1 -0
  12. moreniius-0.8.0/tests/test_bifrost_nexus_structure.py +29 -0
  13. {moreniius-0.6.3 → moreniius-0.8.0}/tests/test_motorized_positions.py +62 -25
  14. moreniius-0.6.3/src/moreniius/mccode/mccode.py +0 -92
  15. moreniius-0.6.3/src/moreniius.egg-info/requires.txt +0 -5
  16. {moreniius-0.6.3 → moreniius-0.8.0}/.github/dependabot.yml +0 -0
  17. {moreniius-0.6.3 → moreniius-0.8.0}/.github/workflows/pip.yml +0 -0
  18. {moreniius-0.6.3 → moreniius-0.8.0}/.github/workflows/wheels.yml +0 -0
  19. {moreniius-0.6.3 → moreniius-0.8.0}/.gitignore +0 -0
  20. {moreniius-0.6.3 → moreniius-0.8.0}/README.md +0 -0
  21. {moreniius-0.6.3 → moreniius-0.8.0}/setup.cfg +0 -0
  22. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/__init__.py +0 -0
  23. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/additions.py +0 -0
  24. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/mccode/__init__.py +0 -0
  25. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/mccode/comp.py +0 -0
  26. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/moreniius.py +0 -0
  27. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/nexus_structure.py +0 -0
  28. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/nxoff.py +0 -0
  29. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius/writer.py +0 -0
  30. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius.egg-info/dependency_links.txt +0 -0
  31. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius.egg-info/entry_points.txt +0 -0
  32. {moreniius-0.6.3 → moreniius-0.8.0}/src/moreniius.egg-info/top_level.txt +0 -0
  33. {moreniius-0.6.3 → moreniius-0.8.0}/tests/test_elliptic_guide_gravity.py +0 -0
  34. {moreniius-0.6.3 → moreniius-0.8.0}/tests/test_nexus_structure.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moreniius
3
- Version: 0.6.3
3
+ Version: 0.8.0
4
4
  Author-email: Gregory Tucker <gregory.tucker@ess.eu>
5
5
  Classifier: License :: OSI Approved :: BSD License
6
6
  Classifier: Development Status :: 2 - Pre-Alpha
@@ -14,8 +14,8 @@ Classifier: Programming Language :: Python :: 3.14
14
14
  Description-Content-Type: text/markdown
15
15
  Requires-Dist: zenlog>=1.1
16
16
  Requires-Dist: platformdirs>=3.11
17
- Requires-Dist: mccode-antlr[hdf5]>=0.17.0
18
- Requires-Dist: nexusformat>=1.0.6
17
+ Requires-Dist: mccode-antlr[hdf5]>=0.17.2
18
+ Requires-Dist: nexusformat>=2.0.0
19
19
  Requires-Dist: networkx
20
20
 
21
21
  # moreniius
@@ -7,8 +7,8 @@ name = "moreniius"
7
7
  dependencies = [
8
8
  'zenlog>=1.1',
9
9
  'platformdirs>=3.11',
10
- 'mccode-antlr[hdf5]>=0.17.0',
11
- 'nexusformat>=1.0.6',
10
+ 'mccode-antlr[hdf5]>=0.17.2',
11
+ 'nexusformat>=2.0.0',
12
12
  'networkx'
13
13
  ]
14
14
  readme = "README.md"
@@ -122,6 +122,9 @@ class NXInstance:
122
122
  if not hasattr(self.nx[name], 'depends_on'):
123
123
  self.nx[name].attrs['depends_on'] = most_dependent
124
124
  most_dependent = outer_transform_dependency(self.nx['transformations'])
125
+ # Note: the depends_on entry for NX class objects is an optional dataset
126
+ # in the HDF5 group. Tools like chexus can verify its presence and
127
+ # scippnexus only uses the dataset member of a group for this.
125
128
  self.nx['depends_on'] = f'transformations/{most_dependent}'
126
129
 
127
130
  def get_nx_type(self):
@@ -131,7 +134,7 @@ class NXInstance:
131
134
  return COMPONENT_CATEGORY_TO_NEXUS[self.obj.type.category]
132
135
  if any(self.obj.type.name.startswith(x) for x in COMPONENT_GROUP_TO_NEXUS):
133
136
  return [t for k, t in COMPONENT_GROUP_TO_NEXUS.items() if self.obj.type.name.startswith(k)][0]
134
- return 'NXnote'
137
+ return 'NXcoordinate_system' if self.transforms else 'NXnote'
135
138
 
136
139
  def default_translation(self):
137
140
  import nexusformat.nexus as nexus
@@ -53,7 +53,19 @@ class NXInstr:
53
53
  return NXfield(str(self.instr))
54
54
 
55
55
  def expr2nx(self, expr: Union[str, Expr, Any]):
56
- from moreniius.utils import link_specifier
56
+ """Intended to convert *Expr* objects to NeXus-representable objects"""
57
+ # FIXME this is called to wrap and re-wrap the same data
58
+ # during translation of a component with properties. It may be worth
59
+ # separating the parameter and component functionality.
60
+ from moreniius.utils import link_specifier, NotNXdict
61
+ from nexusformat.nexus import NXlog
62
+ if hasattr(expr, '_value') and isinstance(getattr(expr, '_value'), NotNXdict):
63
+ # Avoid unwrapping the non-NX dictionary at this stage since it is
64
+ # silently converted to a string-like thing which as an __iter__ property
65
+ return expr
66
+ if isinstance(expr, NXlog):
67
+ # Do not decompose a value if we already wrapped it in NXlog
68
+ return expr
57
69
  if not isinstance(expr, str) and hasattr(expr, '__iter__'):
58
70
  parts = [self.expr2nx(x) for x in expr]
59
71
  return tuple(parts) if isinstance(expr, tuple) else parts
@@ -68,6 +80,10 @@ class NXInstr:
68
80
  return evaluated.value
69
81
 
70
82
  dependencies = [par.name for par in self.instr.parameters if evaluated.depends_on(par.name)]
83
+ if len(dependencies) == 1 and str(expr) == str(dependencies[0]):
84
+ from moreniius.utils import linked_nxlog
85
+ return linked_nxlog(f'{self.nxlog_root}/{dependencies[0]}')
86
+
71
87
  if len(dependencies):
72
88
  log.warn(f'The expression {expr} depends on instrument parameter(s) {dependencies}\n'
73
89
  f'A link will be inserted for each; make sure their values are stored at {self.nxlog_root}/')
@@ -77,36 +93,53 @@ class NXInstr:
77
93
  return str(expr)
78
94
 
79
95
  def make_nx(self, nx_class, *args, **kwargs):
96
+ from nexusformat.nexus import NXlog
97
+ from moreniius.utils import NotNXdict
80
98
  nx_args = [self.expr2nx(expr) for expr in args]
81
99
  nx_kwargs = {name: self.expr2nx(expr) for name, expr in kwargs.items()}
82
- # logged parameters are sometimes requested as NXfield objects, but should be links to the real NXlog
83
- if nx_class == NXfield and len(nx_args) == 1 and isinstance(nx_args[0], NXcollection) and \
84
- 'expression' in nx_args[0]:
85
- not_expr = [x for x in nx_args[0] if x != 'expression']
100
+
101
+ # logged parameters are sometimes requested as NXfields, but should be NXlogs
102
+ want_log = nx_class == NXfield and len(nx_args) == 1
103
+ nx_arg = nx_args[0] if want_log else None
104
+ if want_log and isinstance(nx_arg, NXlog):
105
+ # The NXlog returned by expr2nx doesn't have the needed attributes:
106
+ for k, v in nx_kwargs.items():
107
+ nx_arg.attrs[k] = v
108
+ return nx_arg
109
+ # Hopefully less often, a collection of links in an NXcollection
110
+ if want_log and isinstance(nx_arg, NXcollection) and 'expression' in nx_arg:
111
+ not_expr = [x for x in nx_arg if x != 'expression']
86
112
  if len(not_expr) == 1:
87
- not_expr_arg = nx_args[0][not_expr[0]]
88
- # if isinstance(not_expr_arg, NXfield):
89
- # # We have and want an NXfield, but it might be missing attributes specified in the nx_kwargs
90
- # # Passing the keywords to the NXfield constructor versus this method is not identical,
91
- # # since some keyword arguments are reserved (and only some of which are noted)
92
- # # Explicit keywords, used in the constructor:
93
- # # value, name, shape, dtype, group, attrs
94
- # # Keywords extracted from the kwargs dict, if present (and all controlling HDF5 file attributes?):
95
- # # chunks, compression, compression_opts, fillvalue, fletcher32, maxshape, scaleoffset, shuffle
96
- # # For now, just assume all keywords provided here are _actually_ attributes for the NXfield
97
- # # which is an extension of a dict, but can *not* use the update method, since the __setitem__
98
- # # method is overridden to wrap inputs in NXattr objects :/
99
- # for k, v in nx_kwargs.items():
100
- # not_expr_arg.attrs[k] = v
101
- # return not_expr_arg
113
+ arg = nx_arg[not_expr[0]]
114
+ if isinstance(arg, NXfield):
115
+ # if this is a link, we should not add any attributes
116
+ # since the filewriter will ignore them
117
+ if hasattr(arg, '_value') and isinstance(d:=getattr(arg, '_value'), NotNXdict) and d.get('module', '') == 'link':
118
+ return arg
119
+ # We have and want an NXfield, but it might be missing attributes specified in the nx_kwargs
120
+ # Passing the keywords to the NXfield constructor versus this method is not identical,
121
+ # since some keyword arguments are reserved (and only some of which are noted)
122
+ # Explicit keywords, used in the constructor:
123
+ # value, name, shape, dtype, group, attrs
124
+ # Keywords extracted from the kwargs dict, if present (and all controlling HDF5 file attributes?):
125
+ # chunks, compression, compression_opts, fillvalue, fletcher32, maxshape, scaleoffset, shuffle
126
+ # For now, just assume all keywords provided here are _actually_ attributes for the NXfield
127
+ # which is an extension of a dict, but can *not* use the update method, since the __setitem__
128
+ # method is overridden to wrap inputs in NXattr objects :/
129
+ for k, v in nx_kwargs.items():
130
+ arg.attrs[k] = v
131
+ return arg
102
132
 
103
133
  # TODO make this return an nx_class once we're sure that nx_kwargs is parseable (no mccode_antlr.Expr)
104
- if all(x in not_expr_arg for x in ('module', 'config')):
134
+ if all(x in arg for x in ('module', 'config')):
105
135
  # This is a file-writer stream directive? So make a group
106
- return NXgroup(entries={not_expr[0]: not_expr_arg}, **nx_kwargs)
136
+ grp = NXgroup(entries={not_expr[0]: arg})
137
+ for attr, val in nx_kwargs.items():
138
+ grp.attrs[attr] = val
139
+ return grp
107
140
  print('!!')
108
- print(not_expr_arg)
109
- return nx_class(not_expr_arg, **nx_kwargs)
141
+ print(arg)
142
+ return nx_class(arg, **nx_kwargs)
110
143
  else:
111
144
  raise RuntimeError('Not sure what I should do here')
112
145
  return nx_class(*nx_args, **nx_kwargs)
@@ -0,0 +1,154 @@
1
+ from nexusformat.nexus import NXfield
2
+ from zenlog import log
3
+ from dataclasses import dataclass, field
4
+ from networkx import DiGraph
5
+ from typing import Union
6
+ from mccode_antlr.instr import Orient
7
+ from .instr import NXInstr
8
+
9
+ log.level('error')
10
+
11
+ @dataclass
12
+ class NXMcCode:
13
+ nx_instr: NXInstr
14
+ origin_name: Union[str, None] = None
15
+ indexes: dict[str, int] = field(default_factory=dict)
16
+ orientations: dict[str, Orient] = field(default_factory=dict)
17
+ graph: Union[DiGraph, None] = None
18
+ reversed_graph: Union[DiGraph, None] = None
19
+
20
+ def __post_init__(self):
21
+ from copy import deepcopy
22
+
23
+ for index, instance in enumerate(self.nx_instr.instr.components):
24
+ self.indexes[instance.name] = index
25
+ # only absolute-positioned or rotated component orientations are needed
26
+ if instance.at_relative[1] is None or instance.rotate_relative[1] is None:
27
+ self.orientations[instance.name] = deepcopy(instance.orientation)
28
+
29
+ # Attempt to re-center all component dependent orientations on the sample
30
+ found = (
31
+ (lambda x: self.origin_name == x.name)
32
+ if self.origin_name else
33
+ (lambda x: 'samples' == x.type.category)
34
+ )
35
+ possible_origins = [instance for instance in
36
+ self.nx_instr.instr.components if found(instance)]
37
+ if not possible_origins:
38
+ msg = '"sample" category components' if self.origin_name is None else f'component named {self.origin_name}'
39
+ log.warn(f'No {msg} in instrument, using ABSOLUTE positions')
40
+ elif self.origin_name is not None and len(possible_origins) > 1:
41
+ log.error(f'{len(possible_origins)} components named {self.origin_name}; using the first')
42
+ elif len(possible_origins) > 1:
43
+ log.warn(f'More than one "sample" category component. Using {possible_origins[0].name} for origin name')
44
+ if possible_origins:
45
+ self.origin_name = possible_origins[0].name
46
+ # find the position _and_ rotation of the origin
47
+ origin = possible_origins[0].orientation
48
+ # remove this from all (absolute) components (re-centering on the origin)
49
+ for name, orientation in self.orientations.items():
50
+ self.orientations[name] = orientation - origin
51
+
52
+ if self.graph is None:
53
+ self.graph = self.build_graph()
54
+ if self.reversed_graph is None:
55
+ self.reversed_graph = self.graph.reverse(copy=True)
56
+
57
+ def transformations(self, name) -> dict[str, NXfield]:
58
+ from mccode_antlr.instr.orientation import Vector, Angles, Parts
59
+ from .orientation import NXOrient, NXParts
60
+
61
+ def abs_ref(ref):
62
+ # FIXME find a better way to ensure this is correct
63
+ return f'/entry/instrument/{ref}'
64
+
65
+ def last_ref(refs: list[tuple[str, NXfield]]) -> str | None:
66
+ try:
67
+ return next(reversed(refs))[0]
68
+ except StopIteration:
69
+ pass
70
+
71
+ at_vec, at_rel = self.nx_instr.instr.components[self.indexes[name]].at_relative
72
+ rot_ang, rot_rel = self.nx_instr.instr.components[self.indexes[name]].rotate_relative
73
+
74
+ nx_orientation = None
75
+ if at_rel is None or rot_rel is None:
76
+ nx_orientation = NXOrient(self.nx_instr, self.orientations[name])
77
+
78
+ if at_rel is None and rot_rel is None:
79
+ # ABSOLUTE definition, so we pull information from self.orientations
80
+ # since we had to remove the (possibly different) origin
81
+ return nx_orientation.transformations(name)
82
+
83
+ trans = []
84
+ if at_rel is not None and rot_rel is not None and at_rel == rot_rel:
85
+ at_vec = Vector(*at_vec) if isinstance(at_vec, tuple) else at_vec
86
+ rot_ang = Angles(*rot_ang) if isinstance(rot_ang, tuple) else rot_ang
87
+ at_parts = Parts.from_at_rotated(at_vec, Angles(), True)
88
+ rot_parts = Parts.from_at_rotated(Vector(), rot_ang, True)
89
+ nx_parts = NXParts(self.nx_instr, at_parts, rot_parts)
90
+ trans.extend(nx_parts.transformations(name, abs_ref(at_rel.name)))
91
+ else:
92
+ raise RuntimeError("All mixed reference-type orientations untested. "
93
+ "Only 'AT (x, y, z) ABSOLUTE ROTATE (a, b, c) REF' might work")
94
+ # elif at_rel is None:
95
+ # # absolute position with relative rotation
96
+ # trans.extend(nx_orientation.position_transformations(name))
97
+ # # Get the _rotation_ of the reference to add here before any new rotation
98
+ # # FIXME this can only work if rot_rel.name is in self.orientations!
99
+ # rel_ori = NXOrient(self.nx_instr, self.orientations[rot_rel.name])
100
+ # trans.extend(rel_ori.rotation_transformations(rot_rel.name, last_ref(trans)))
101
+ # # Now add our relative rotation onto the referenced rotation
102
+ # rot_ang = Angles(*rot_ang) if isinstance(rot_ang, tuple) else rot_ang
103
+ # rot = Parts(Parts.from_at_rotated(Vector(), rot_ang, True).stack()).reduce()
104
+ # nx_parts = NXParts(self.nx_instr, rot, rot)
105
+ # trans.extend(nx_parts.rotation_transformations(name, last_ref(trans)))
106
+ # elif rot_rel is None:
107
+ # # relative position with absolute rotations
108
+ # raise RuntimeError("I can not handle this yet")
109
+ # else:
110
+ # # relative position and rotation but different references.
111
+ # raise RuntimeError("I cnat no handle this ytet")
112
+
113
+ return {k: v for k, v in trans}
114
+
115
+ def inputs(self, name):
116
+ """Return the other end of edges ending at the named node"""
117
+ return list(self.reversed_graph[name])
118
+
119
+ def outputs(self, name):
120
+ """Return the other end of edges starting at the named node"""
121
+ return list(self.graph[name])
122
+
123
+ def component(self, name, only_nx=True):
124
+ """Return a NeXus NXcomponent corresponding to the named McStas component instance"""
125
+ from .instance import NXInstance
126
+ instance = self.nx_instr.instr.components[self.indexes[name]]
127
+ transformations = self.transformations(name)
128
+ nxinst = NXInstance(self.nx_instr, instance, self.indexes[name], transformations, only_nx=only_nx)
129
+ if transformations and nxinst.nx['transformations'] != transformations and name in self.orientations:
130
+ # if the component modifed the transformations group, make sure we don't use our version again
131
+ del self.orientations[name]
132
+ if len(inputs := self.inputs(name)):
133
+ nxinst.nx.attrs['inputs'] = inputs
134
+ if len(outputs := self.outputs(name)):
135
+ nxinst.nx.attrs['outputs'] = outputs
136
+ return nxinst
137
+
138
+ def instrument(self, only_nx=True):
139
+ from nexusformat.nexus import NXinstrument
140
+ nx = NXinstrument() # this is a NeXus class
141
+ nx['mcstas'] = self.nx_instr.to_nx()
142
+ for name in self.indexes.keys():
143
+ nx[name] = self.component(name, only_nx=only_nx).nx
144
+
145
+ return nx
146
+
147
+ def build_graph(self):
148
+ # FIXME expand this to a full-description if/when McCode includes graph information
149
+ graph = DiGraph()
150
+ names = [x.name for x in self.nx_instr.instr.components]
151
+ graph.add_nodes_from(names)
152
+ # By default, any McCode instrument is a linear object:
153
+ graph.add_edges_from([(names[i], names[i+1]) for i in range(len(names)-1)])
154
+ return graph
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from zenlog import log
4
- from dataclasses import dataclass
4
+ from dataclasses import dataclass, field
5
5
  from mccode_antlr.instr import Orient, Parts, Part
6
6
  from nexusformat.nexus import NXfield
7
7
  from .instr import NXInstr
@@ -73,27 +73,52 @@ class NXParts:
73
73
  position: Parts
74
74
  rotation: Parts
75
75
 
76
- def transformations(self, name: str) -> list[tuple[str, NXfield]]:
77
- nxt = []
78
- dep = '.'
79
- for index, o in enumerate(self.position.stack()):
80
- nxt.extend(NXPart(self.instr, o).transformations(f'{name}_t{index}', dep))
81
- dep = nxt[-1][0] if len(nxt) and len(nxt[-1]) else '.'
82
- for index, o in enumerate(self.rotation.stack()):
83
- nxt.extend(NXPart(self.instr, o).transformations(f'{name}_r{index}', dep))
84
- dep = nxt[-1][0] if len(nxt) and len(nxt[-1]) else '.'
85
- return nxt
76
+ def _transformations(self, name: str, dep: str, typ: str, stack):
77
+ nx_transformations = []
78
+ for i, op in enumerate(stack):
79
+ parts = NXPart(self.instr, op).transformations(f'{name}_{typ}{i}', dep)
80
+ nx_transformations.extend(parts)
81
+ if len(parts) and len(parts[-1]):
82
+ dep = parts[-1][0]
83
+ return nx_transformations
84
+
85
+ def position_transformations(self, name: str, dep: str | None = None ) -> list[tuple[str, NXfield]]:
86
+ dep = dep or '.'
87
+ return self._transformations(name, dep, 't', self.position.stack())
88
+
89
+ def rotation_transformations(self, name: str, dep: str | None = None) -> list[tuple[str, NXfield]]:
90
+ dep = dep or '.'
91
+ return self._transformations(name, dep, 'r', self.rotation.stack())
92
+
93
+ def transformations(self, name: str, dep: str | None = None) -> list[tuple[str, NXfield]]:
94
+ parts = self.position_transformations(name, dep=dep)
95
+ # If there were any positioning transformations, we need to update
96
+ # the dependency chained name, otherwise it should stay the same
97
+ dep = parts[-1][0] if len(parts) and len(parts[-1]) else dep
98
+ return parts + self.rotation_transformations(name, dep=dep)
86
99
 
87
100
 
88
101
  @dataclass
89
102
  class NXOrient:
90
103
  instr: NXInstr
91
104
  do: Orient
105
+ nx_parts: NXParts | None = None
92
106
 
93
- def transformations(self, name: str) -> dict[str, NXfield]:
107
+ def __post_init__(self):
94
108
  # collapse all possible chained orientation information
95
109
  # But keep the rotations and translations separate
96
110
  pos, rot = self.do.position_parts(), self.do.rotation_parts()
97
111
  # make an ordered list of the requisite NXfield entries
98
- nxt = NXParts(self.instr, pos, rot).transformations(name)
99
- return {k: v for k, v in nxt}
112
+ self.nx_parts = NXParts(self.instr, pos, rot)
113
+
114
+ def transformations(self, name: str) -> dict[str, NXfield]:
115
+ # make an ordered list of the requisite NXfield entries, and turn it into a dict
116
+ return {k: v for k, v in self.nx_parts.transformations(name)}
117
+
118
+ def position_transformations(self, name: str, dep: str | None = None) -> list[tuple[str, NXfield]]:
119
+ return self.nx_parts.position_transformations(name, dep)
120
+
121
+ def rotation_transformations(self, name: str, dep: str | None = None) -> list[tuple[str, NXfield]]:
122
+ return self.nx_parts.rotation_transformations(name, dep)
123
+
124
+
@@ -1,5 +1,5 @@
1
1
  from mccode_antlr.instr import Instance
2
- from nexusformat.nexus import NXevent_data, NXfield
2
+ from nexusformat.nexus import NXevent_data, NXfield, NXlog
3
3
 
4
4
  class NotNXdict:
5
5
  """Wrapper class to prevent NXfield-parsing of the held dictionary"""
@@ -16,6 +16,10 @@ class NotNXdict:
16
16
  from json import dumps
17
17
  return dumps(self.value)
18
18
 
19
+ def get(self, item, default=None):
20
+ return self.value.get(item, default)
21
+
22
+
19
23
 
20
24
  def outer_transform_dependency(transformations):
21
25
  """For a NXtransformations group, find the most-dependent transformation name
@@ -175,6 +179,26 @@ def ev44_event_data_group(source: str, topic: str) -> NXevent_data:
175
179
  return NXevent_data(data=ess_flatbuffer_specifier('ev44', {'source': source, 'topic': topic}))
176
180
 
177
181
 
182
+ def nxlog_data_links(source: str):
183
+ """
184
+ Return link module specifications for the datasets inserted by a f144 module
185
+ """
186
+ datasets = (
187
+ 'alarm_message', 'alarm_severity', 'alarm_time', 'average_value',
188
+ 'connection_status', 'connection_status_time', 'cue_index',
189
+ 'cue_timestamp_zero', 'description', 'maximum_value', 'minimum_value',
190
+ 'time', 'value'
191
+ )
192
+ return {k: link_specifier(k, f'{source}/{k}') for k in datasets}
193
+
194
+
195
+ def linked_nxlog(source: str, attrs: dict | None = None) -> NXlog:
196
+ nxlog = NXlog(**nxlog_data_links(source))
197
+ if attrs:
198
+ nxlog.attrs.update(attrs)
199
+ return nxlog
200
+
201
+
178
202
  def link_specifier(name: str, source: str) -> NotNXdict:
179
203
  """
180
204
  Constructs a specifier to insert a NeXus link into an ESS produced NeXus file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moreniius
3
- Version: 0.6.3
3
+ Version: 0.8.0
4
4
  Author-email: Gregory Tucker <gregory.tucker@ess.eu>
5
5
  Classifier: License :: OSI Approved :: BSD License
6
6
  Classifier: Development Status :: 2 - Pre-Alpha
@@ -14,8 +14,8 @@ Classifier: Programming Language :: Python :: 3.14
14
14
  Description-Content-Type: text/markdown
15
15
  Requires-Dist: zenlog>=1.1
16
16
  Requires-Dist: platformdirs>=3.11
17
- Requires-Dist: mccode-antlr[hdf5]>=0.17.0
18
- Requires-Dist: nexusformat>=1.0.6
17
+ Requires-Dist: mccode-antlr[hdf5]>=0.17.2
18
+ Requires-Dist: nexusformat>=2.0.0
19
19
  Requires-Dist: networkx
20
20
 
21
21
  # moreniius
@@ -23,6 +23,8 @@ src/moreniius/mccode/instance.py
23
23
  src/moreniius/mccode/instr.py
24
24
  src/moreniius/mccode/mccode.py
25
25
  src/moreniius/mccode/orientation.py
26
+ tests/bifrost.instr.json
27
+ tests/test_bifrost_nexus_structure.py
26
28
  tests/test_elliptic_guide_gravity.py
27
29
  tests/test_motorized_positions.py
28
30
  tests/test_nexus_structure.py
@@ -0,0 +1,5 @@
1
+ zenlog>=1.1
2
+ platformdirs>=3.11
3
+ mccode-antlr[hdf5]>=0.17.2
4
+ nexusformat>=2.0.0
5
+ networkx