Skip to content

Add the ability to save a file that can be read by Mantid - #108

Draft
AnthonyLim23 wants to merge 37 commits into
mainfrom
62_save_3
Draft

AnthonyLim23 wants to merge 37 commits into
mainfrom
62_save_3

Conversation

@AnthonyLim23

@AnthonyLim23 AnthonyLim23 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

This PR adds code to clean up the current data (should not have null values in the event file) and add the missing meta-data so it can be loaded into Mantid.

The Python script I wrote for creating the reference file has been added under tools, for completeness.

The code that cleans and adds meta-data to the output is in Rust. At present it does not handle Dwell and this will be fixed as part of #77.

I will check the output against the schema and add the code needed to output a json of the missing/incorrect data as part of #109.

I have the following left:

  • Add example to docs
  • Add Mantid system test
  • Clean up the code

To test:

from MNeuEventLib import Data
file = 'HIFI00207745_events.nxs'

data = Data(file, 64)
result = data.calculate()
data.save_nexus('HIFI42.nxs')

Then check HIFI42.nxs loads in both Mantid's muon analysis GUI and Wimda.

@AnthonyLim23 AnthonyLim23 added feature New feature or request data Concerns handling data input/output labels Sep 2, 2026

@alexhroom alexhroom left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very cursory initial look - i'll test properly once you've added system tests.

as a general note, the docstring style is inconsistent: in some places it has

Parameters
----------

and in some places it has

## Parameters

both of which are not consistent to the docstring style used in the rest of the code (numpy style)

Comment thread tools/make_default.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this file start with a docstring/comment explaining what it does and what it's for?

Comment thread tests/Mantid/integration_test.py Outdated

def test_Mantid_install():
"""
Lets first test that Mantid works

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Lets first test that Mantid works
Test that Mantid has correctly installed.

Function docstrings should be imperatives.

Comment thread src/data/save/sanitise/nexus_data.rs
Comment thread src/data/save/instrument.rs
Comment thread src/test_utils.rs
Comment thread src/data/save/utils.rs Outdated
Comment thread src/data/save/sanitise/utils.rs
@github-actions github-actions Bot added the Has Conflicts This PR has a merge conflict. label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

👋 Hi, @AnthonyLim23,

Conflicts have been detected against the base branch. Please rebase your branch against the base branch.


This message is automatically generated by prince-chrismc/label-merge-conflicts-action so don't hesitate to report issues/improvements there.

@github-actions github-actions Bot removed the Has Conflicts This PR has a merge conflict. label Sep 8, 2026
Comment thread src/data/save/wimda.rs
@AnthonyLim23

Copy link
Copy Markdown
Collaborator Author

I still need to update the docs and mantid test

@@ -0,0 +1,3 @@
//! Code for saving outputs to file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be more specific?

continue;
} else if let Ok(_dataset) = sample.dataset(&name) {
if name == "thickness" {
let default = Array1::from_shape_vec(1, vec![0.0f32]).unwrap();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let default = Array1::from_shape_vec(1, vec![0.0f32]).unwrap();
let default = Array1::from_shape_vec(1, vec![0.0f32])?;

replace unwraps with ? in functions that return Result

Comment on lines +87 to +111
create_default_dataset(&source_parent.group(name).unwrap(), dest, name, shapes)?;
Ok(())
} else if source_parent.dataset(name).is_ok() && dest.dataset(name).is_ok() {
// if dataset exists in both files
println!("{} dataset already exists", name);
let src_ds = source_parent.dataset(name)?;
let dst_ds = dest.dataset(name)?;
for att_name in src_ds.attr_names()? {
println!("copy attribute {}", att_name);
copy_attr(&src_ds, &dst_ds, &att_name)?;
}
Ok(())
} else if source_parent.dataset(name).is_ok() && dest.dataset(name).is_err() {
// if dataset exists in source but not in destination
println!("copy dataset {}", name);
source_parent.dataset(name)?.copy_to(dest, name)?;
Ok(())
} else if source_parent.group(name).is_ok() && dest.group(name).is_ok() {
// if group exists in both files
println!("group {} exists in both files, going deeper", name);
for member in source_parent.group(name).unwrap().member_names()? {
set_defaults(
&source_parent.group(name).unwrap(),
&dest.group(name).unwrap(),
member.as_str(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
create_default_dataset(&source_parent.group(name).unwrap(), dest, name, shapes)?;
Ok(())
} else if source_parent.dataset(name).is_ok() && dest.dataset(name).is_ok() {
// if dataset exists in both files
println!("{} dataset already exists", name);
let src_ds = source_parent.dataset(name)?;
let dst_ds = dest.dataset(name)?;
for att_name in src_ds.attr_names()? {
println!("copy attribute {}", att_name);
copy_attr(&src_ds, &dst_ds, &att_name)?;
}
Ok(())
} else if source_parent.dataset(name).is_ok() && dest.dataset(name).is_err() {
// if dataset exists in source but not in destination
println!("copy dataset {}", name);
source_parent.dataset(name)?.copy_to(dest, name)?;
Ok(())
} else if source_parent.group(name).is_ok() && dest.group(name).is_ok() {
// if group exists in both files
println!("group {} exists in both files, going deeper", name);
for member in source_parent.group(name).unwrap().member_names()? {
set_defaults(
&source_parent.group(name).unwrap(),
&dest.group(name).unwrap(),
member.as_str(),
create_default_dataset(&source_parent.group(name)?, dest, name, shapes)?;
Ok(())
} else if source_parent.dataset(name).is_ok() && dest.dataset(name).is_ok() {
// if dataset exists in both files
println!("{} dataset already exists", name);
let src_ds = source_parent.dataset(name)?;
let dst_ds = dest.dataset(name)?;
for att_name in src_ds.attr_names()? {
println!("copy attribute {}", att_name);
copy_attr(&src_ds, &dst_ds, &att_name)?;
}
Ok(())
} else if source_parent.dataset(name).is_ok() && dest.dataset(name).is_err() {
// if dataset exists in source but not in destination
println!("copy dataset {}", name);
source_parent.dataset(name)?.copy_to(dest, name)?;
Ok(())
} else if source_parent.group(name).is_ok() && dest.group(name).is_ok() {
// if group exists in both files
println!("group {} exists in both files, going deeper", name);
for member in source_parent.group(name)?.member_names()? {
set_defaults(
&source_parent.group(name)?,
&dest.group(name)?,
member.as_str(),

replace unwraps with ? in functions that return Result

Comment on lines +30 to +51
let name = default.name().split("dataset_").last().unwrap().to_string();
let key: String = default
.dataset("shape")
.unwrap()
.read_scalar::<hdf5::types::VarLenUnicode>()
.unwrap()
.as_str()
.to_string();
let len: usize = *shapes.get(&key).unwrap();
let dtype: &hdf5::types::VarLenUnicode =
&default.dataset("dtype").unwrap().read_scalar().unwrap();
if dtype.as_str() == "int32" {
let default_value = default.dataset("default").unwrap().read_scalar::<i32>()?;
if let Err(e) = add_array(dest, &Array1::from_elem(len, default_value), &name) {
eprintln!("error: {e}");
eprintln!("chain: {e:?}");
};
} else if dtype.as_str() == "float32" {
let default_value = default.dataset("default").unwrap().read_scalar::<f32>()?;
add_array(dest, &Array1::from_elem(len, default_value), &name)?;
} else if dtype.as_str() == "float64" {
let default_value = default.dataset("default").unwrap().read_scalar::<f64>()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let name = default.name().split("dataset_").last().unwrap().to_string();
let key: String = default
.dataset("shape")
.unwrap()
.read_scalar::<hdf5::types::VarLenUnicode>()
.unwrap()
.as_str()
.to_string();
let len: usize = *shapes.get(&key).unwrap();
let dtype: &hdf5::types::VarLenUnicode =
&default.dataset("dtype").unwrap().read_scalar().unwrap();
if dtype.as_str() == "int32" {
let default_value = default.dataset("default").unwrap().read_scalar::<i32>()?;
if let Err(e) = add_array(dest, &Array1::from_elem(len, default_value), &name) {
eprintln!("error: {e}");
eprintln!("chain: {e:?}");
};
} else if dtype.as_str() == "float32" {
let default_value = default.dataset("default").unwrap().read_scalar::<f32>()?;
add_array(dest, &Array1::from_elem(len, default_value), &name)?;
} else if dtype.as_str() == "float64" {
let default_value = default.dataset("default").unwrap().read_scalar::<f64>()?;
let name = default.name().split("dataset_").last()?.to_string();
let key: String = default
.dataset("shape")?
.read_scalar::<hdf5::types::VarLenUnicode>()?
.as_str()
.to_string();
let len: usize = *shapes.get(&key)?;
let dtype: &hdf5::types::VarLenUnicode =
&default.dataset("dtype")?.read_scalar()?;
if dtype.as_str() == "int32" {
let default_value = default.dataset("default")?.read_scalar::<i32>()?;
if let Err(e) = add_array(dest, &Array1::from_elem(len, default_value), &name) {
eprintln!("error: {e}");
eprintln!("chain: {e:?}");
};
} else if dtype.as_str() == "float32" {
let default_value = default.dataset("default")?.read_scalar::<f32>()?;
add_array(dest, &Array1::from_elem(len, default_value), &name)?;
} else if dtype.as_str() == "float64" {
let default_value = default.dataset("default")?.read_scalar::<f64>()?;

replace unwraps with ? in functions that return Result

println!("Attribute '{name}' already exists in destination dataset, skipping");
return Ok(());
}
let cname = CString::new(name).unwrap();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let cname = CString::new(name).unwrap();
let cname = CString::new(name)?;

replace unwraps with ? in functions that return Result

Comment thread tools/make_default.py
this includes the file path), then when saving
event data to histograms, the command would be

data.save(<output>, autofill=Ture, ref_file=<ref>)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data.save(<output>, autofill=Ture, ref_file=<ref>)
data.save(<output>, autofill=True, ref_file=<ref>)

/// The destination group
/// name: &str
/// The name of the attribute to copy
unsafe fn _copy_attr(src: &Location, dst: &Location, name: &str) -> Result<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add some code comments explaining why using the low-level bindings is necessary in each case?

new_ds.write(new_data)?;

for attr_name in &attr_names {
let _ = copy_attr(&old, &new_ds, attr_name);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let _ = copy_attr(&old, &new_ds, attr_name);
copy_attr(&old, &new_ds, attr_name)?;

using let _ ignores the error if copy_attr returns an error

/// ----------
/// file_name: &str
/// The name of the file to get the period information from
pub fn get_p_info(file_name: &str) -> Result<(usize, usize)> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change name to get_period_info

Comment thread src/batch_interface.rs
/// Contains "correct" data that should be copied to the output file.
/// This is only need it the reference file needed is not the standard
/// muon nexus v2 file. The ref_file is generated from tools/make_default.py.
pub fn save_nexus(&self, filename: String, ref_file: String) -> Result<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this function actually doing? it doesn't seem to actually save any of the data, just save the default values to an already-created data file. maybe it needs a better name and docstring?

also does it need to be in the python API? will a user ever use this directly or is it only used through save?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data Concerns handling data input/output feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants