moreniius 0.5.2__tar.gz → 0.6.1__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 (30) hide show
  1. moreniius-0.6.1/.github/dependabot.yml +17 -0
  2. {moreniius-0.5.2 → moreniius-0.6.1}/.github/workflows/pip.yml +4 -9
  3. {moreniius-0.5.2 → moreniius-0.6.1}/.github/workflows/wheels.yml +4 -4
  4. {moreniius-0.5.2/src/moreniius.egg-info → moreniius-0.6.1}/PKG-INFO +4 -3
  5. {moreniius-0.5.2 → moreniius-0.6.1}/pyproject.toml +5 -4
  6. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/mccode/comp.py +77 -14
  7. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/mccode/mccode.py +36 -10
  8. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/moreniius.py +4 -2
  9. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/writer.py +13 -4
  10. {moreniius-0.5.2 → moreniius-0.6.1/src/moreniius.egg-info}/PKG-INFO +4 -3
  11. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius.egg-info/SOURCES.txt +2 -0
  12. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius.egg-info/requires.txt +2 -1
  13. moreniius-0.6.1/tests/test_elliptic_guide_gravity.py +179 -0
  14. {moreniius-0.5.2 → moreniius-0.6.1}/tests/test_nexus_structure.py +3 -3
  15. {moreniius-0.5.2 → moreniius-0.6.1}/.gitignore +0 -0
  16. {moreniius-0.5.2 → moreniius-0.6.1}/README.md +0 -0
  17. {moreniius-0.5.2 → moreniius-0.6.1}/setup.cfg +0 -0
  18. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/__init__.py +0 -0
  19. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/additions.py +0 -0
  20. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/mccode/__init__.py +0 -0
  21. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/mccode/instance.py +0 -0
  22. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/mccode/instr.py +0 -0
  23. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/mccode/orientation.py +0 -0
  24. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/nexus_structure.py +0 -0
  25. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/nxoff.py +0 -0
  26. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius/utils.py +0 -0
  27. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius.egg-info/dependency_links.txt +0 -0
  28. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius.egg-info/entry_points.txt +0 -0
  29. {moreniius-0.5.2 → moreniius-0.6.1}/src/moreniius.egg-info/top_level.txt +0 -0
  30. {moreniius-0.5.2 → moreniius-0.6.1}/tests/test_motorized_positions.py +0 -0
@@ -0,0 +1,17 @@
1
+ # Please see the documentation for all configuration options:
2
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
3
+
4
+ version: 2
5
+ updates:
6
+ - package-ecosystem: "github-actions" # See documentation for possible values
7
+ directory: "/" # Location of package manifests
8
+ schedule:
9
+ interval: "weekly"
10
+ - package-ecosystem: "pip"
11
+ directory: "/"
12
+ schedule:
13
+ interval: "daily"
14
+ groups:
15
+ python-packages:
16
+ patterns:
17
+ - "*"
@@ -15,24 +15,19 @@ jobs:
15
15
  fail-fast: false
16
16
  matrix:
17
17
  platform: [windows-latest, macos-latest, ubuntu-latest]
18
- python-version: ["3.9", "3.10", "3.11", "3.12"]
18
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
19
19
 
20
20
  steps:
21
- - uses: actions/checkout@v4
21
+ - uses: actions/checkout@v6
22
22
 
23
- - uses: actions/setup-python@v5
23
+ - uses: actions/setup-python@v6
24
24
  with:
25
25
  python-version: ${{ matrix.python-version }}
26
26
 
27
- - name: Set min macOS version
28
- if: runner.os == 'macOS'
29
- run: |
30
- echo "MACOS_DEPLOYMENT_TARGET=10.14" >> $GITHUB_ENV
31
-
32
27
  - name: Build and install
33
28
  run: pip install --verbose .
34
29
 
35
30
  - name: Test
36
31
  run: |
37
- python -m pip install pytest git+https://github.com/g5t/mccode-to-kafka.git
32
+ python -m pip install pytest mccode-to-kafka
38
33
  python -m pytest
@@ -15,7 +15,7 @@ jobs:
15
15
  name: Build SDist and Wheel
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
- - uses: actions/checkout@v4
18
+ - uses: actions/checkout@v6
19
19
  with:
20
20
  fetch-depth: 0
21
21
  submodules: true
@@ -26,7 +26,7 @@ jobs:
26
26
  - name: Check metadata
27
27
  run: pipx run twine check dist/*
28
28
 
29
- - uses: actions/upload-artifact@v4
29
+ - uses: actions/upload-artifact@v6
30
30
  with:
31
31
  path: dist/*
32
32
 
@@ -42,9 +42,9 @@ jobs:
42
42
  if: github.event_name == 'release' && github.event.action == 'published'
43
43
 
44
44
  steps:
45
- - uses: actions/setup-python@v5
45
+ - uses: actions/setup-python@v6
46
46
 
47
- - uses: actions/download-artifact@v4
47
+ - uses: actions/download-artifact@v7
48
48
  with:
49
49
  name: artifact
50
50
  path: dist
@@ -1,21 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moreniius
3
- Version: 0.5.2
3
+ Version: 0.6.1
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
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3 :: Only
9
- Classifier: Programming Language :: Python :: 3.9
10
9
  Classifier: Programming Language :: Python :: 3.10
11
10
  Classifier: Programming Language :: Python :: 3.11
12
11
  Classifier: Programming Language :: Python :: 3.12
13
12
  Classifier: Programming Language :: Python :: 3.13
13
+ 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.15.2
17
+ Requires-Dist: mccode-antlr[hdf5]>=0.16.1
18
18
  Requires-Dist: nexusformat>=1.0.6
19
+ Requires-Dist: networkx
19
20
 
20
21
  # moreniius
21
22
  A project to contain custom components required to use `eniius` to produce `NeXus Structure` `JSON` from `mccode-antlr` simulated instruments.
@@ -7,8 +7,9 @@ name = "moreniius"
7
7
  dependencies = [
8
8
  'zenlog>=1.1',
9
9
  'platformdirs>=3.11',
10
- 'mccode-antlr[hdf5]>=0.15.2',
11
- 'nexusformat>=1.0.6'
10
+ 'mccode-antlr[hdf5]>=0.16.1',
11
+ 'nexusformat>=1.0.6',
12
+ 'networkx'
12
13
  ]
13
14
  readme = "README.md"
14
15
  authors = [
@@ -19,11 +20,11 @@ classifiers = [
19
20
  "Development Status :: 2 - Pre-Alpha",
20
21
  "Programming Language :: Python :: 3",
21
22
  "Programming Language :: Python :: 3 :: Only",
22
- "Programming Language :: Python :: 3.9",
23
23
  "Programming Language :: Python :: 3.10",
24
24
  "Programming Language :: Python :: 3.11",
25
25
  "Programming Language :: Python :: 3.12",
26
26
  "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
27
28
  ]
28
29
  dynamic = ["version"]
29
30
 
@@ -37,11 +38,11 @@ legacy_tox_ini = """
37
38
  [tox]
38
39
  min_version = 4.0
39
40
  env_list =
41
+ py314
40
42
  py313
41
43
  py312
42
44
  py311
43
45
  py310
44
- py39
45
46
  type
46
47
 
47
48
  [testenv]
@@ -95,27 +95,41 @@ def diskchopper_translator(nxinstance):
95
95
  return nxinstance.make_nx(NXdisk_chopper, slit_edges=NXfield(nx_slit_edges, units='degrees'), **resolve_parameter_links(pars))
96
96
 
97
97
 
98
- def elliptic_guide_gravity_translator(nxinstance):
99
- from nexusformat.nexus import NXguide
98
+ def _ellipse_vertices_faces(major_x, minor_x, offset_x, major_y, minor_y, offset_y, l, n=10):
99
+ """
100
+ Create vertices and faces for an elliptical guide with given parameters.
101
+
102
+ Parameters
103
+ ----------
104
+ major_x : float
105
+ Major axis half-length in the x-direction.
106
+ minor_x : float
107
+ Minor axis half-length in the x-direction.
108
+ offset_x : float
109
+ Offset from the end of the ellipse to the guide entrance in the x-direction.
110
+ major_y : float
111
+ Major axis half-length in the y-direction.
112
+ minor_y : float
113
+ Minor axis half-length in the y-direction.
114
+ offset_y : float
115
+ Offset from the end of the ellipse to the guide entrance in the y-direction.
116
+ l : float
117
+ Length of the guide. l <= 2*major_x - offset_x and l <= 2*major_y - offset_y
118
+ n : int, optional
119
+ Number of segments along the length of the guide. Default is 10.
120
+ """
100
121
  from numpy import arange, sqrt
101
- from moreniius.nxoff import NXoff
102
- if not '"mid"' == nxinstance.obj.get_parameter('dimensionsAt'):
103
- log.warn('Only midpoint geometry supported by Elliptic_guide_gravity translator')
104
- log.info(f'The current guide has {nxinstance.obj.get_parameter("dimensionsAt")} specified')
105
122
 
106
- def ellipse_width(minor, distance, at):
107
- major = sqrt((distance / 2) ** 2 + minor ** 2)
123
+ def ellipse_width(minor, major, at):
108
124
  return 0 if abs(at) > major else minor * sqrt(1 - (at / major) ** 2)
109
125
 
110
- pars = dict(xw='xwidth', xi='linxw', xo='loutxw', yw='yheight', yi='linyh', yo='loutyh', l='l')
111
- p = {k: nxinstance.parameter(v) for k, v in pars.items()}
112
- n = 10
113
126
  rings = arange(n + 1) / n
114
127
  faces, vertices = [], []
115
128
  for x in rings:
116
- w = ellipse_width(p['xw'] / 2, p['xi'] + p['l'] + p['xo'], p['xi'] / 2 + (x - 0.5) * p['l'] - p['xo'] / 2)
117
- h = ellipse_width(p['yw'] / 2, p['yi'] + p['l'] + p['yo'], p['yi'] / 2 + (x - 0.5) * p['l'] - p['yo'] / 2)
118
- z = x * p['l']
129
+ z = x * l
130
+ w = ellipse_width(minor_x, major_x, offset_x - minor_x + z)
131
+ h = ellipse_width(minor_y, major_y, offset_y - minor_y + x)
132
+
119
133
  vertices.extend([[-w, -h, z], [-w, h, z], [w, h, z], [w, -h, z]])
120
134
 
121
135
  # These are only the guide faces (that is, the inner faces of the sides of the guide housing)
@@ -124,6 +138,55 @@ def elliptic_guide_gravity_translator(nxinstance):
124
138
  j0, j1, j2, j3, j4, j5, j6, j7 = [4 * i + k for k in range(8)]
125
139
  faces.extend([[j0, j1, j5, j4], [j1, j2, j6, j5], [j2, j3, j7, j6], [j3, j0, j4, j7]])
126
140
 
141
+ return vertices, faces
142
+
143
+ def _ellipse_parameters_from_widths(nxinstance):
144
+ from numpy import sqrt
145
+
146
+ def parameters(which, w, i, o, l):
147
+ foci = i + l + o
148
+ offset = foci / 2 - i
149
+ if 'mid' in which:
150
+ minor = w / 2
151
+ major = sqrt(foci ** 2 + minor ** 2) / 2
152
+ else:
153
+ t, b = (o, i) if 'entrance' in which else (i, o)
154
+ t += l
155
+ w /= 2
156
+ b = sqrt(b * b + w * w / 4) + sqrt(t * t + w * w / 4)
157
+ major = b / 2
158
+ minor = sqrt(b * b - foci * foci) / 2
159
+ return major, minor, offset
160
+
161
+ pars = dict(xw='xwidth', xi='linxw', xo='loutxw', yw='yheight', yi='linyh', yo='loutyh', l='l')
162
+ p = {k: nxinstance.parameter(v) for k, v in pars.items()}
163
+
164
+ dim_at = str(nxinstance.obj.get_parameter('dimensionsAt').value)
165
+ major_x, minor_x, offset_x = parameters(dim_at, p['xw'], p['xi'], p['xo'], p['l'])
166
+ major_y, minor_y, offset_y = parameters(dim_at, p['yw'], p['yi'], p['yo'], p['l'])
167
+
168
+ return major_x, minor_x, offset_x, major_y, minor_y, offset_y, p['l']
169
+
170
+
171
+ def elliptic_guide_gravity_translator(nxinstance):
172
+ from nexusformat.nexus import NXguide
173
+ from moreniius.nxoff import NXoff
174
+
175
+ ellipse_pars = [f'{f}{s}' for f in ('majorAxis', 'minorAxis', 'majorAxisOffset') for s in ('xw', 'yh')]
176
+ if all(nxinstance.obj.defines_parameter(p) for p in ellipse_pars):
177
+ # we can use the specified ellipse parameters directly
178
+ major_x = nxinstance.parameter('majorAxisxw')
179
+ minor_x = nxinstance.parameter('minorAxisxw')
180
+ offset_x = nxinstance.parameter('majorAxisOffsetxw')
181
+ major_y = nxinstance.parameter('majorAxisyh')
182
+ minor_y = nxinstance.parameter('minorAxisyh')
183
+ offset_y = nxinstance.parameter('majorAxisOffsetyh')
184
+ l = nxinstance.parameter('l')
185
+ else:
186
+ major_x, minor_x, offset_x, major_y, minor_y, offset_y, l = _ellipse_parameters_from_widths(nxinstance)
187
+
188
+ vertices, faces = _ellipse_vertices_faces(major_x, minor_x, offset_x, major_y, minor_y, offset_y, l, n=10)
189
+
127
190
  nx_vertices = [[nxinstance.expr2nx(expr) for expr in vector] for vector in vertices]
128
191
  nx_faces = [[nxinstance.expr2nx(expr) for expr in face] for face in faces]
129
192
 
@@ -1,5 +1,7 @@
1
1
  from zenlog import log
2
2
  from dataclasses import dataclass, field
3
+ from networkx import DiGraph
4
+ from typing import Union
3
5
  from mccode_antlr.instr import Orient
4
6
  from .instr import NXInstr
5
7
 
@@ -9,9 +11,11 @@ log.level('error')
9
11
  @dataclass
10
12
  class NXMcCode:
11
13
  nx_instr: NXInstr
12
- origin_name: str = None
14
+ origin_name: Union[str, None] = None
13
15
  indexes: dict[str, int] = field(default_factory=dict)
14
16
  orientations: dict[str, Orient] = field(default_factory=dict)
17
+ graph: Union[DiGraph, None] = None
18
+ reversed_graph: Union[DiGraph, None] = None
15
19
 
16
20
  def __post_init__(self):
17
21
  from copy import deepcopy
@@ -37,30 +41,52 @@ class NXMcCode:
37
41
  for name in self.orientations:
38
42
  self.orientations[name] = self.orientations[name] - origin
39
43
 
44
+ if self.graph is None:
45
+ self.graph = self.build_graph()
46
+ if self.reversed_graph is None:
47
+ self.reversed_graph = self.graph.reverse(copy=True)
48
+
40
49
  def transformations(self, name):
41
50
  from .orientation import NXOrient
42
51
  return NXOrient(self.nx_instr, self.orientations[name]).transformations(name)
43
52
 
53
+ def inputs(self, name):
54
+ """Return the other end of edges ending at the named node"""
55
+ return list(self.reversed_graph[name])
56
+
57
+ def outputs(self, name):
58
+ """Return the other end of edges starting at the named node"""
59
+ return list(self.graph[name])
60
+
44
61
  def component(self, name, only_nx=True):
45
62
  """Return a NeXus NXcomponent corresponding to the named McStas component instance"""
46
63
  from .instance import NXInstance
47
64
  instance = self.nx_instr.instr.components[self.indexes[name]]
48
65
  transformations = self.transformations(name)
49
- nx = NXInstance(self.nx_instr, instance, self.indexes[name], transformations, only_nx=only_nx)
50
- if transformations and nx.nx['transformations'] != transformations:
66
+ nxinst = NXInstance(self.nx_instr, instance, self.indexes[name], transformations, only_nx=only_nx)
67
+ if transformations and nxinst.nx['transformations'] != transformations:
51
68
  # if the component modifed the transformations group, make sure we don't use our version again
52
69
  del self.orientations[name]
53
- return nx
70
+ if len(inputs := self.inputs(name)):
71
+ nxinst.nx.attrs['inputs'] = inputs
72
+ if len(outputs := self.outputs(name)):
73
+ nxinst.nx.attrs['outputs'] = outputs
74
+ return nxinst
54
75
 
55
76
  def instrument(self, only_nx=True):
56
- from .instr import NXInstr
57
77
  from nexusformat.nexus import NXinstrument
58
78
  nx = NXinstrument() # this is a NeXus class
59
79
  nx['mcstas'] = self.nx_instr.to_nx()
60
- # hack the McCode component index into the name of the NeXus group
61
- width = len(str(max(self.indexes.values())))
62
- for name, index in self.indexes.items():
63
- nx_name = f'{index:0{width}d}_{name}'
64
- nx[nx_name] = self.component(name, only_nx=only_nx).nx
80
+ for name in self.indexes.keys():
81
+ nx[name] = self.component(name, only_nx=only_nx).nx
65
82
 
66
83
  return nx
84
+
85
+ def build_graph(self):
86
+ # FIXME expand this to a full-description if/when McCode includes graph information
87
+ graph = DiGraph()
88
+ names = [x.name for x in self.nx_instr.instr.components]
89
+ graph.add_nodes_from(names)
90
+ # By default, any McCode instrument is a linear object:
91
+ graph.add_edges_from([(names[i], names[i+1]) for i in range(len(names)-1)])
92
+ return graph
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+ from networkx import Graph
2
3
  from mccode_antlr.instr import Instr
3
4
 
4
5
 
@@ -13,12 +14,13 @@ class MorEniius:
13
14
  origin: str | None = None,
14
15
  only_nx: bool = False,
15
16
  nxlog_root: str | None = None,
16
- absolute_depends_on: bool = False
17
+ absolute_depends_on: bool = False,
18
+ graph: Graph | None = None,
17
19
  ):
18
20
  from nexusformat.nexus import NXfield
19
21
  from .mccode import NXMcCode, NXInstr
20
22
  nxlog_root = nxlog_root or '/entry/parameters'
21
- nx_mccode = NXMcCode(NXInstr(instr, nxlog_root=nxlog_root), origin_name=origin)
23
+ nx_mccode = NXMcCode(NXInstr(instr, nxlog_root=nxlog_root), origin_name=origin, graph=graph)
22
24
  nxs_obj = nx_mccode.instrument(only_nx=only_nx)
23
25
  nxs_obj['name'] = NXfield(value=instr.name)
24
26
  return cls(nxs_obj, only_nx=only_nx, absolute_depends_on=absolute_depends_on)
@@ -2,7 +2,7 @@ from zenlog import log
2
2
 
3
3
 
4
4
  def convert_types(obj, only_nx=True):
5
- from numpy import dtype, ndarray
5
+ from numpy import dtype, ndarray, array
6
6
  from nexusformat.nexus import NXattr
7
7
  py_data_type = type(obj)
8
8
  np_data_type = dtype(py_data_type)
@@ -24,11 +24,16 @@ def convert_types(obj, only_nx=True):
24
24
  val = val.tolist()
25
25
  if obj.dtype == 'object':
26
26
  (tp, vl) = (dtype(type(obj.nxdata)).name, val)
27
+ # If still 'object', this will throw an error below
28
+ if tp == 'object' and isinstance(val, list):
29
+ tp = dtype(type(val[0])).name
27
30
  else:
28
31
  (tp, vl) = (obj.dtype, val)
29
32
  elif not only_nx and hasattr(obj, 'to_json_dict'):
30
33
  # Shoe-horn in an object-defined dictionary:
31
34
  tp, vl = None, obj.to_json_dict()
35
+ elif isinstance(obj, list):
36
+ return convert_types(array(obj))
32
37
  else:
33
38
  raise RuntimeError(f'unrecognised type {py_data_type} / {np_data_type} for {repr(obj)}')
34
39
  else:
@@ -38,7 +43,7 @@ def convert_types(obj, only_nx=True):
38
43
  elif tp == 'float64':
39
44
  tp = 'double'
40
45
  elif tp == 'object':
41
- raise RuntimeError(f'Internal logical error attempting to convert {obj}')
46
+ raise RuntimeError(f'Internal logical error attempting to convert {obj} of type {type(obj)}')
42
47
  elif tp == 'int':
43
48
  tp = 'int64'
44
49
  elif tp == 'float':
@@ -124,8 +129,12 @@ class Writer:
124
129
  attrs = [dict(name='NX_class', dtype='string', values=obj.nxclass)]
125
130
  if len(list(obj)):
126
131
  entry['children'] = self._to_json_dict(obj, only_nx=only_nx, absolute_depends_on=absolute_depends_on)
127
- for n, v in obj.attrs.items():
128
- typ, val = convert_types(v, only_nx)
132
+ for n in obj.attrs:
133
+ typ, val = convert_types(obj.attrs[n], only_nx)
134
+ # FIXME accessing an attribute value via the dict values gives
135
+ # a NXattr object *not* the underlying value!?
136
+ # for n, v in obj.attrs.items():
137
+ # typ, val = convert_types(v, only_nx)
129
138
  if absolute_depends_on and n == 'depends_on' and '/' != val[0]:
130
139
  val = _to_absolute(top_obj.nxpath, val)
131
140
  attrs.append(dict(name=n, dtype=typ, values=val) if typ else val)
@@ -1,21 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moreniius
3
- Version: 0.5.2
3
+ Version: 0.6.1
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
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3 :: Only
9
- Classifier: Programming Language :: Python :: 3.9
10
9
  Classifier: Programming Language :: Python :: 3.10
11
10
  Classifier: Programming Language :: Python :: 3.11
12
11
  Classifier: Programming Language :: Python :: 3.12
13
12
  Classifier: Programming Language :: Python :: 3.13
13
+ 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.15.2
17
+ Requires-Dist: mccode-antlr[hdf5]>=0.16.1
18
18
  Requires-Dist: nexusformat>=1.0.6
19
+ Requires-Dist: networkx
19
20
 
20
21
  # moreniius
21
22
  A project to contain custom components required to use `eniius` to produce `NeXus Structure` `JSON` from `mccode-antlr` simulated instruments.
@@ -1,6 +1,7 @@
1
1
  .gitignore
2
2
  README.md
3
3
  pyproject.toml
4
+ .github/dependabot.yml
4
5
  .github/workflows/pip.yml
5
6
  .github/workflows/wheels.yml
6
7
  src/moreniius/__init__.py
@@ -22,5 +23,6 @@ src/moreniius/mccode/instance.py
22
23
  src/moreniius/mccode/instr.py
23
24
  src/moreniius/mccode/mccode.py
24
25
  src/moreniius/mccode/orientation.py
26
+ tests/test_elliptic_guide_gravity.py
25
27
  tests/test_motorized_positions.py
26
28
  tests/test_nexus_structure.py
@@ -1,4 +1,5 @@
1
1
  zenlog>=1.1
2
2
  platformdirs>=3.11
3
- mccode-antlr[hdf5]>=0.15.2
3
+ mccode-antlr[hdf5]>=0.16.1
4
4
  nexusformat>=1.0.6
5
+ networkx
@@ -0,0 +1,179 @@
1
+ import pytest
2
+ from nexusformat.nexus import NXguide
3
+
4
+
5
+ def make_elliptic_guide_instrument(use_explicit_ellipse_pars=False):
6
+ """Create an instrument with an Elliptic_guide_gravity component."""
7
+ from mccode_antlr import Flavor
8
+ from mccode_antlr.assembler import Assembler
9
+
10
+ inst = Assembler('elliptic_guide_test', flavor=Flavor.MCSTAS)
11
+
12
+ inst.component('origin', 'Arm', at=(0, 0, 0))
13
+ inst.component('source', 'Source_simple', at=[(0, 0, 0), 'origin'])
14
+
15
+ if use_explicit_ellipse_pars:
16
+ # Use explicit ellipse axis parameters
17
+ inst.component(
18
+ 'elliptic_guide', 'Elliptic_guide_gravity',
19
+ at=[(0, 0, 1), 'source'],
20
+ parameters={
21
+ 'l': 2.0,
22
+ 'majorAxisxw': 1.5,
23
+ 'minorAxisxw': 0.05,
24
+ 'majorAxisoffsetxw': 0.5,
25
+ 'majorAxisyh': 1.5,
26
+ 'minorAxisyh': 0.06,
27
+ 'majorAxisoffsetyh': 0.5,
28
+ }
29
+ )
30
+ else:
31
+ # Use width/height based parameters with dimensionsAt
32
+ inst.component(
33
+ 'elliptic_guide', 'Elliptic_guide_gravity',
34
+ at=[(0, 0, 1), 'source'],
35
+ parameters={
36
+ 'l': 2.0,
37
+ 'xwidth': 0.06,
38
+ 'yheight': 0.08,
39
+ 'linxw': 1.0,
40
+ 'loutxw': 1.0,
41
+ 'linyh': 1.0,
42
+ 'loutyh': 1.0,
43
+ 'dimensionsAt': '"mid"',
44
+ }
45
+ )
46
+
47
+ return inst.instrument
48
+
49
+
50
+ def test_elliptic_guide_gravity_translator_from_widths():
51
+ """Test the elliptic guide translator using width/height parameters."""
52
+ import moreniius
53
+
54
+ instr = make_elliptic_guide_instrument(use_explicit_ellipse_pars=False)
55
+ me = moreniius.MorEniius.from_mccode(
56
+ instr, origin='origin', only_nx=False, absolute_depends_on=True
57
+ )
58
+
59
+ assert me is not None
60
+ assert 'elliptic_guide' in me.nx
61
+
62
+ guide = me.nx['elliptic_guide']
63
+ assert isinstance(guide, NXguide)
64
+
65
+ # Should have OFF_GEOMETRY
66
+ assert 'OFF_GEOMETRY' in guide
67
+
68
+
69
+ def test_elliptic_guide_gravity_translator_explicit_pars():
70
+ """Test the elliptic guide translator using explicit ellipse axis parameters."""
71
+ import moreniius
72
+
73
+ instr = make_elliptic_guide_instrument(use_explicit_ellipse_pars=True)
74
+ me = moreniius.MorEniius.from_mccode(
75
+ instr, origin='origin', only_nx=False, absolute_depends_on=True
76
+ )
77
+
78
+ assert me is not None
79
+ assert 'elliptic_guide' in me.nx
80
+
81
+ guide = me.nx['elliptic_guide']
82
+ assert isinstance(guide, NXguide)
83
+
84
+ # Should have OFF_GEOMETRY
85
+ assert 'OFF_GEOMETRY' in guide
86
+
87
+
88
+ def test_elliptic_guide_geometry_has_vertices_and_faces():
89
+ """Test that the generated geometry has vertices and faces."""
90
+ import moreniius
91
+
92
+ instr = make_elliptic_guide_instrument(use_explicit_ellipse_pars=True)
93
+ me = moreniius.MorEniius.from_mccode(
94
+ instr, origin='origin', only_nx=False, absolute_depends_on=True
95
+ )
96
+
97
+ guide = me.nx['elliptic_guide']
98
+ geometry = guide['OFF_GEOMETRY']
99
+
100
+ # NXoff_geometry should have vertices, faces, and winding_order
101
+ assert 'vertices' in geometry
102
+ assert 'faces' in geometry
103
+ assert 'winding_order' in geometry
104
+
105
+ # Vertices should be a 2D array (n_vertices, 3)
106
+ vertices = geometry['vertices'].nxdata
107
+ assert vertices.ndim == 2
108
+ assert vertices.shape[1] == 3
109
+
110
+ # With n=10 segments, we should have 11 rings of 4 vertices each = 44 vertices
111
+ assert vertices.shape[0] == 44
112
+
113
+
114
+ def test_elliptic_guide_nexus_structure():
115
+ """Test that the elliptic guide is correctly represented in NeXus structure output."""
116
+ import moreniius
117
+
118
+ instr = make_elliptic_guide_instrument(use_explicit_ellipse_pars=True)
119
+ me = moreniius.MorEniius.from_mccode(
120
+ instr, origin='origin', only_nx=False, absolute_depends_on=True
121
+ )
122
+
123
+ ns = me.to_nexus_structure()
124
+
125
+ # Navigate to the instrument level
126
+ assert 'children' in ns
127
+ entry = ns['children'][0]
128
+ assert entry['name'] == 'entry'
129
+
130
+ instrument = entry['children'][0]
131
+ assert instrument['name'] == 'instrument'
132
+
133
+ # Find the elliptic_guide component
134
+ guide_groups = [c for c in instrument['children'] if c.get('name') == 'elliptic_guide']
135
+ assert len(guide_groups) == 1
136
+
137
+ guide = guide_groups[0]
138
+ assert guide['type'] == 'group'
139
+
140
+ # Check that it has the NXguide class attribute
141
+ guide_attrs = guide.get('attributes', [])
142
+ class_attrs = [a for a in guide_attrs if a.get('name') == 'NX_class']
143
+ assert len(class_attrs) == 1
144
+ assert class_attrs[0]['values'] == 'NXguide'
145
+
146
+
147
+ def test_ellipse_vertices_faces_function():
148
+ """Test the _ellipse_vertices_faces helper function directly."""
149
+ from moreniius.mccode.comp import _ellipse_vertices_faces
150
+
151
+ major_x, minor_x, offset_x = 1.5, 0.05, 0.5
152
+ major_y, minor_y, offset_y = 1.5, 0.06, 0.5
153
+ l = 2.0
154
+ n = 5
155
+
156
+ vertices, faces = _ellipse_vertices_faces(
157
+ major_x, minor_x, offset_x,
158
+ major_y, minor_y, offset_y,
159
+ l, n=n
160
+ )
161
+
162
+ # Should have (n+1) rings of 4 vertices each
163
+ assert len(vertices) == (n + 1) * 4
164
+
165
+ # Each ring creates 4 faces (top, bottom, left, right of guide)
166
+ # For n segments, we have n * 4 faces
167
+ assert len(faces) == n * 4
168
+
169
+ # Each vertex should have 3 coordinates
170
+ assert all(len(v) == 3 for v in vertices)
171
+
172
+ # Each face should reference 4 vertex indices
173
+ assert all(len(f) == 4 for f in faces)
174
+
175
+ # z-coordinates should range from 0 to l
176
+ z_coords = [v[2] for v in vertices]
177
+ assert min(z_coords) == pytest.approx(0.0)
178
+ assert max(z_coords) == pytest.approx(l)
179
+
@@ -28,7 +28,7 @@ class NexusStrctureTestCase(unittest.TestCase):
28
28
  END
29
29
  """
30
30
  self.instr = parse_mcstas_instr(instr)
31
- self.structures = {'2_mon0': m0, '5_mon1': m1}
31
+ self.structures = {'mon0': m0, 'mon1': m1}
32
32
 
33
33
  def test_moreniius(self):
34
34
  from moreniius import MorEniius
@@ -72,7 +72,7 @@ class NexusStrctureTestCase(unittest.TestCase):
72
72
  nx = nx['children'][3]
73
73
  for x in group_keys:
74
74
  self.assertTrue(x in nx)
75
- self.assertEqual(nx['name'], '2_mon0')
75
+ self.assertEqual(nx['name'], 'mon0')
76
76
  self.assertEqual(len(nx['children']), 4) # removed mcstas child
77
77
  nx = nx['children'][1] # this is now a NXdata group
78
78
  self.assertTrue('attributes' in nx)
@@ -80,7 +80,7 @@ class NexusStrctureTestCase(unittest.TestCase):
80
80
  self.assertEqual(nx['attributes'][0]['name'], 'NX_class')
81
81
  self.assertEqual(nx['attributes'][0]['values'], 'NXdata')
82
82
  nx = nx['children'][0]
83
- self.assertEqual(self.structures['2_mon0'], nx)
83
+ self.assertEqual(self.structures['mon0'], nx)
84
84
 
85
85
 
86
86
  if __name__ == '__main__':
File without changes
File without changes
File without changes