From c25c6afaafc5accd3aa2f7402940ed4ec1379a70 Mon Sep 17 00:00:00 2001 From: Johannes Elferich Date: Sun, 30 Mar 2025 20:33:51 -0400 Subject: [PATCH 1/7] Simple write function and documentation update --- README.md | 89 +++----------------------- docs/index.md | 121 ++--------------------------------- docs/object_api.md | 93 +++++++++++++++++++++++++++ docs/slicer_angles.md | 22 +++++++ src/imodmodel/__init__.py | 4 +- src/imodmodel/functions.py | 11 ++++ src/imodmodel/models.py | 46 ++++++++++++- tests/test_functional_api.py | 11 ++++ 8 files changed, 199 insertions(+), 198 deletions(-) create mode 100644 docs/object_api.md create mode 100644 docs/slicer_angles.md diff --git a/README.md b/README.md index d713d18..5102329 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ # imodmodel -[![License](https://img.shields.io/pypi/l/imodmodel.svg?color=green)](https://github.com/alisterburt/imodmodel/raw/main/LICENSE) +[![License](https://img.shields.io/pypi/l/imodmodel.svg?color=green)](https://github.com/teamtomo/imodmodel/raw/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/imodmodel.svg?color=green)](https://pypi.org/project/imodmodel) [![Python Version](https://img.shields.io/pypi/pyversions/imodmodel.svg?color=green)](https://python.org) -[![CI](https://github.com/alisterburt/imodmodel/actions/workflows/test_and_deploy.yml/badge.svg)](https://github.com/alisterburt/imodmodel/actions/workflows/test_and_deploy.yml) +[![CI](https://github.com/teamtomo/imodmodel/actions/workflows/test_and_deploy.yml/badge.svg)](https://github.com/teamtomo/imodmodel/actions/workflows/test_and_deploy.yml) -Read [IMOD model files](https://bio3d.colorado.edu/imod/doc/binspec.html) +Read and write [IMOD model files](https://bio3d.colorado.edu/imod/doc/binspec.html) as [pandas dataframes](https://pandas.pydata.org/) in Python. ## Usage -### As pandas DataFrame +### Read IMOD models as pandas DataFrame ```python import imodmodel @@ -32,87 +32,14 @@ Out[3]: ``` - -### As ImodModel object +### Write IMOD models from a pandas DataFrame ```python -from imodmodel import ImodModel -model = ImodModel.from_file("my_model_file.mod") +imodmodel.write(df, 'my_new_modelfile.mod') ``` -```ipython -In [3]: model.objects[0].contours[0].points -Out[3]: -array([[ 6.875, 62.875, 124. ], ...]) - -In [4]: model.objects[0].meshes[0].vertices -Out[4]: -array([[ 6.87500000e+00, 6.28750000e+01, 1.24000000e+02], ...]) - -In [5]: model.objects[0].meshes[0].indices -Out[5]: -array([[156, 18, 152], ...]) - -In [6]: model.objects[0].meshes[0].face_values -Out[6]: -array([0., 0., 35.22094345, ...]) -``` - -That's it! - -### Create and save model files - -```python -model = ImodModel( - objects=[ - Object( - color=(0.0,1.0,0.0), - header = ObjectHeader( - flags=ObjectFlags( - scattered=True - ) - ), - contours = [ - Contour( - points=np.array([ - [4.5,8.0,0.0], - [9.0,8.0,0.0] - ]) - - ) - ] - ), - Object( - color=(0.0,0.0,1.0), - header=ObjectHeader( - pdrawsize=0 - ), - contours=[ - Contour( - points = np.column_stack((6.75 + 6.75 * np.cos(np.linspace(0, 2 * np.pi, 200, endpoint=False)), 6.75 + 6.75 * np.sin(np.linspace(0, 2 * np.pi, 200, endpoint=False)), np.zeros(200))) - ) - ] - ), - Object( - color=(1.0,0.0,0.0), - header=ObjectHeader( - pdrawsize=0, - flags=ObjectFlags( - open=True - ) - ), - - contours=[ - Contour( - points = np.column_stack((6.75 + 4.75 * np.cos(np.linspace(1 * np.pi, 2 * np.pi, 100, endpoint=False)), 6.75 + 4.75 * np.sin(np.linspace( 1 * np.pi, 2 * np.pi, 100, endpoint=False)), np.zeros(100))) - ) - ] - ) - ] -) - -model.to_file("smiley.mod") -``` +For more advanced use cases we also provide an object-based API. +Please consult our [Documentation](https://teamtomo.org/imodmodel/). ## Installation `imodmodel` can be installed from the [Python Package Index](https://pypi.org/) (PyPI) diff --git a/docs/index.md b/docs/index.md index fd04a9f..bc218d7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,16 +1,18 @@ # Overview -[![License](https://img.shields.io/pypi/l/imodmodel.svg?color=green)](https://github.com/alisterburt/imodmodel/raw/main/LICENSE) +[![License](https://img.shields.io/pypi/l/imodmodel.svg?color=green)](https://github.com/teamtomo/imodmodel/raw/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/imodmodel.svg?color=green)](https://pypi.org/project/imodmodel) [![Python Version](https://img.shields.io/pypi/pyversions/imodmodel.svg?color=green)](https://python.org) -[![CI](https://github.com/alisterburt/imodmodel/actions/workflows/test_and_deploy.yml/badge.svg)](https://github.com/alisterburt/imodmodel/actions/workflows/ci.yml) +[![CI](https://github.com/teamtomo/imodmodel/actions/workflows/test_and_deploy.yml/badge.svg)](https://github.com/teamtomo/imodmodel/actions/workflows/ci.yml) -Read [IMOD model files](https://bio3d.colorado.edu/imod/doc/binspec.html) +Read and write [IMOD model files](https://bio3d.colorado.edu/imod/doc/binspec.html) as [pandas dataframes](https://pandas.pydata.org/) in Python. ## Usage +### Read IMOD models as pandas DataFrame + ```python import imodmodel @@ -29,119 +31,10 @@ Out[3]: ``` - -Slicer angles saved in the [slicer window](https://bio3d.colorado.edu/imod/doc/3dmodHelp/slicer.html) -are stored in the IMOD binary file with both centerpoints and angles. - -These annotations can be read in by setting `annotation='slicer_angle'` when calling `imodmodel.read()` - -```python -import imodmodel - -df = imodmodel.read('file_with_slicer_angles.mod', annotation='slicer_angles') -``` - -```ipython -In [3]: df.head() -Out[3]: - object_id slicer_angle_id time x_rot y_rot z_rot center_x center_y center_z label -0 0 0 1 13.100000 0.0 -30.200001 235.519577 682.744141 302.0 -0 0 1 1 -41.400002 0.0 -47.700001 221.942444 661.193237 327.0 -0 0 2 1 -41.400002 0.0 -41.799999 232.790726 671.332031 327.0 -0 0 3 1 -35.500000 0.0 -36.000000 240.129181 679.927795 324.0 -``` - -## ImodModel - -The resulting dataframe from `imodmodel.read()` contains only information about the contours or slicer angles. -The full set of information from the imod model file can be parsed using `ImodModel` +### Write IMOD models from a pandas DataFrame ```python -from imodmodel import ImodModel - -my_model = ImodModel.from_file("my_model_file.mod") -``` - -```ipython -in [3]: my_model.model_field_set -out[3]: -{'id', 'extra', 'objects', 'slicer_angles', 'header'} -``` - -### my_model.id - -`my_model.id` contains the IMOD file id and the version id - -```ipython -in [4]: my_model.id -out[4]: -ID(IMOD_file_id='IMOD', version_id='V1.2') -``` - -### my_model.header - -`my_model.header` is contains the model structure data mainly used by IMOD. - -```ipython -in [5]: my_model.header -out[5]: -ModelHeader(name='IMOD-NewModel', xmax=956, ymax=924, zmax=300, objsize=3, flags=62976, drawmode=1, -mousemode=1, blacklevel=145, whitelevel=173, xoffset=0.0, yoffset=0.0, zoffset=0.0, xscale=1.0, yscale=10, -zscale=1.0, object=2, contour=-1, point=-1, res=3, thresh=128, pixelsize=1.9733333587646484, units=-9, -csum=704518946, alpha=0.0, beta=0.0, gamma=0.0) -``` - -### my_model.objects - -`my_model.objects` is a `list` IMOD objects. - -```ipython -in [6]: my_model.objects[0].header -out[6]: -ObjectHeader(name='', extra_data=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], contsize=0, -flags=402653184, axis=0, drawmode=1, red=0.0, green=1.0, blue=0.0, pdrawsize=0, symbol=1, symsize=3, -linewidth2=1, linewidth=1, linesty=0, symflags=0, sympad=0, trans=0, meshsize=0, surfsize=0) -``` - -This is where object values like contours, meshes, and IMAT information are located. - -```ipython -in [7]: my_model.objects[1].meshes[0].indices -out[7]: -array([[38, 40, 52], - [38, 52, 50], - [50, 52, 64], - [50, 64, 60], - ..., - [ 4, 10, 26], - [ 4, 26, 20], - [20, 26, 38], - [20, 38, 32]]) -``` - -```ipython -in [8]: my_model.objects[1].imat -out[8]: -IMAT(ambient=102, diffuse=255, specular=127, shininess=4, fillred=0, fillgreen=0, fillblue=0, -quality=0, mat2=0, valblack=0, valwhite=255, matflags2=0, mat3b3=0) -``` - -```ipython -in [9]: my_model.objects[1].contours[0].points -out[9]: -array([[367.00006104, 661.83343506, 134. ], - [415.66674805, 667.83343506, 134. ], - [474.33340454, 662.50012207, 134. ]]) -``` - -### my_model.slicer_angles - -`my_model.slicer_angles` is a `list` of slicer angles. - -```ipython -in [10]: my_model.slicer_angles[0] -out[10]: -SLAN(time=1, angles=(0.0, 0.0, 0.0), center=(533.5, 717.0, 126.0), label='\x00') +imodmodel.write(df, 'my_new_modelfile.mod') ``` That's it! diff --git a/docs/object_api.md b/docs/object_api.md new file mode 100644 index 0000000..69109c6 --- /dev/null +++ b/docs/object_api.md @@ -0,0 +1,93 @@ + +# Object-based API + +The resulting dataframe from `imodmodel.read()` contains only information about the contours or slicer angles. +The full set of information from the imod model file can be parsed using `ImodModel` + +```python +from imodmodel import ImodModel + +my_model = ImodModel.from_file("my_model_file.mod") +``` + +```ipython +in [3]: my_model.model_field_set +out[3]: +{'id', 'extra', 'objects', 'slicer_angles', 'header'} +``` + +### my_model.id + +`my_model.id` contains the IMOD file id and the version id + +```ipython +in [4]: my_model.id +out[4]: +ID(IMOD_file_id='IMOD', version_id='V1.2') +``` + +### my_model.header + +`my_model.header` is contains the model structure data mainly used by IMOD. + +```ipython +in [5]: my_model.header +out[5]: +ModelHeader(name='IMOD-NewModel', xmax=956, ymax=924, zmax=300, objsize=3, flags=62976, drawmode=1, +mousemode=1, blacklevel=145, whitelevel=173, xoffset=0.0, yoffset=0.0, zoffset=0.0, xscale=1.0, yscale=10, +zscale=1.0, object=2, contour=-1, point=-1, res=3, thresh=128, pixelsize=1.9733333587646484, units=-9, +csum=704518946, alpha=0.0, beta=0.0, gamma=0.0) +``` + +### my_model.objects + +`my_model.objects` is a `list` IMOD objects. + +```ipython +in [6]: my_model.objects[0].header +out[6]: +ObjectHeader(name='', extra_data=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], contsize=0, +flags=402653184, axis=0, drawmode=1, red=0.0, green=1.0, blue=0.0, pdrawsize=0, symbol=1, symsize=3, +linewidth2=1, linewidth=1, linesty=0, symflags=0, sympad=0, trans=0, meshsize=0, surfsize=0) +``` + +This is where object values like contours, meshes, and IMAT information are located. + +```ipython +in [7]: my_model.objects[1].meshes[0].indices +out[7]: +array([[38, 40, 52], + [38, 52, 50], + [50, 52, 64], + [50, 64, 60], + ..., + [ 4, 10, 26], + [ 4, 26, 20], + [20, 26, 38], + [20, 38, 32]]) +``` + +```ipython +in [8]: my_model.objects[1].imat +out[8]: +IMAT(ambient=102, diffuse=255, specular=127, shininess=4, fillred=0, fillgreen=0, fillblue=0, +quality=0, mat2=0, valblack=0, valwhite=255, matflags2=0, mat3b3=0) +``` + +```ipython +in [9]: my_model.objects[1].contours[0].points +out[9]: +array([[367.00006104, 661.83343506, 134. ], + [415.66674805, 667.83343506, 134. ], + [474.33340454, 662.50012207, 134. ]]) +``` + +### my_model.slicer_angles + +`my_model.slicer_angles` is a `list` of slicer angles. + +```ipython +in [10]: my_model.slicer_angles[0] +out[10]: +SLAN(time=1, angles=(0.0, 0.0, 0.0), center=(533.5, 717.0, 126.0), label='\x00') +``` diff --git a/docs/slicer_angles.md b/docs/slicer_angles.md new file mode 100644 index 0000000..f8907d8 --- /dev/null +++ b/docs/slicer_angles.md @@ -0,0 +1,22 @@ +# Slicer Angles + +Slicer angles saved in the [slicer window](https://bio3d.colorado.edu/imod/doc/3dmodHelp/slicer.html) +are stored in the IMOD binary file with both centerpoints and angles. + +These annotations can be read in by setting `annotation='slicer_angle'` when calling `imodmodel.read()` + +```python +import imodmodel + +df = imodmodel.read('file_with_slicer_angles.mod', annotation='slicer_angles') +``` + +```ipython +In [3]: df.head() +Out[3]: + object_id slicer_angle_id time x_rot y_rot z_rot center_x center_y center_z label +0 0 0 1 13.100000 0.0 -30.200001 235.519577 682.744141 302.0 +0 0 1 1 -41.400002 0.0 -47.700001 221.942444 661.193237 327.0 +0 0 2 1 -41.400002 0.0 -41.799999 232.790726 671.332031 327.0 +0 0 3 1 -35.500000 0.0 -36.000000 240.129181 679.927795 324.0 +``` diff --git a/src/imodmodel/__init__.py b/src/imodmodel/__init__.py index df5e407..7abf5ae 100644 --- a/src/imodmodel/__init__.py +++ b/src/imodmodel/__init__.py @@ -1,4 +1,4 @@ -from .functions import read +from .functions import read, write from .models import ImodModel -__all__ = ["read", "ImodModel"] +__all__ = ["read", "write", "ImodModel"] diff --git a/src/imodmodel/functions.py b/src/imodmodel/functions.py index 67541fe..15466be 100644 --- a/src/imodmodel/functions.py +++ b/src/imodmodel/functions.py @@ -14,3 +14,14 @@ def read(filename: os.PathLike, annotation: str = 'contour') -> pd.DataFrame: """ model = ImodModel.from_file(filename) return model_to_dataframe(model,annotation) + +def write(dataframe: pd.DataFrame, filename: os.PathLike) -> None: + """Write a pandas DataFrame to an IMOD model file. + + Parameters + ---------- + dataframe : pandas DataFrame to write + filename : filename to write + """ + model = ImodModel.from_dataframe(dataframe) + model.to_file(filename) \ No newline at end of file diff --git a/src/imodmodel/models.py b/src/imodmodel/models.py index c2fbe7c..874039d 100644 --- a/src/imodmodel/models.py +++ b/src/imodmodel/models.py @@ -1,8 +1,10 @@ +from enum import Enum import os import warnings from typing import List, Optional, Tuple, Union import numpy as np +import pandas as pd from pydantic import BaseModel, ConfigDict, field_validator, model_validator class ID(BaseModel): @@ -69,6 +71,12 @@ class ContourFlags(IntFlagModel): connect_invert: bool = False +class ContourType(Enum): + OPEN = "open" + CLOSED = "closed" + SCATTERED = "scattered" + + class ContourHeader(BaseModel): """https://bio3d.colorado.edu/imod/doc/binspec.html""" psize: int = 0 @@ -486,7 +494,43 @@ def from_file(cls, filename: os.PathLike): from .parsers import parse_model with open(filename, 'rb') as file: return parse_model(file) - + + @classmethod + def from_dataframe(cls, dataframe: pd.DataFrame, type: ContourType = ContourType.SCATTERED): + """Read an IMOD model from a pandas DataFrame.""" + + # Ensure the DataFrame has the required columns + required_columns = ['x', 'y', 'z'] + for col in required_columns: + if col not in dataframe.columns: + raise ValueError(f"DataFrame must contain the column '{col}'") + if "object_id" not in dataframe.columns: + dataframe["object_id"] = 0 + if "contour_id" not in dataframe.columns: + dataframe["contour_id"] = 0 + + model = ImodModel() + for object_id, object_group in dataframe.groupby("object_id"): + obj = Object() + for contour_id, contour_group in object_group.groupby("contour_id"): + contour = Contour( + points=contour_group[['x', 'y', 'z']].values, + ) + obj.contours.append(contour) + if type == ContourType.SCATTERED: + obj.header.flags.scattered = True + obj.header.flags.open = False + elif type == ContourType.OPEN: + obj.header.flags.scattered = False + obj.header.flags.open = True + elif type == ContourType.CLOSED: + obj.header.flags.scattered = False + obj.header.flags.open = False + obj.update_sizes() + model.objects.append(obj) + model.update_sizes() + return model + def to_file(self, filename: os.PathLike): """Write an IMOD model to disk.""" from .writers import write_model diff --git a/tests/test_functional_api.py b/tests/test_functional_api.py index 6c062e3..5799baa 100644 --- a/tests/test_functional_api.py +++ b/tests/test_functional_api.py @@ -34,3 +34,14 @@ def test_unknown_annotation(two_contour_model_file): """Check that an error is raised if an unknown annotation is requested.""" with pytest.raises(ValueError, match="Unknown annotation type: unknown"): df = imodmodel.read(two_contour_model_file, annotation='unknown') + +def test_write(two_contour_model_file,tmp_path): + """Check that a model can be written to a file.""" + # Read the original model + original_model = imodmodel.read(two_contour_model_file) + # Write the model to a new file + imodmodel.write(original_model, tmp_path / "test_model.imod") + # Read the new model + new_model = imodmodel.read(tmp_path / "test_model.imod") + # Check that the new model matches the original + assert original_model.equals(new_model) From 8f5afcbd99e4769c2db2148edad0355f22a4d5ea Mon Sep 17 00:00:00 2001 From: Johannes Elferich Date: Sun, 30 Mar 2025 21:09:35 -0400 Subject: [PATCH 2/7] Note about Imodmodel coordinate system --- docs/assets/coordinates_multiz.png | Bin 0 -> 15048 bytes docs/assets/coordinates_xyz.png | Bin 0 -> 13617 bytes docs/z_coordinate_system_note.md | 46 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 docs/assets/coordinates_multiz.png create mode 100644 docs/assets/coordinates_xyz.png create mode 100644 docs/z_coordinate_system_note.md diff --git a/docs/assets/coordinates_multiz.png b/docs/assets/coordinates_multiz.png new file mode 100644 index 0000000000000000000000000000000000000000..d165e0e403da3a6cad3b89b8f621732eb85fb53a GIT binary patch literal 15048 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 NlAJ26M8+uSe*p^s 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 + ![Multi-Z window](assets/coordinates_multiz.png){ width="600" } +
The resulting position of points in XY slices
+ + +
+ ![XYZ window](assets/coordinates_xyz.png){ width="450" } +
The resulting position of (0,0,0) in XYZ slices
+
+ +Since many other programs place (0,0,0) in the center of the first voxel, +one might consider adding (0.5,0.5,0) to the coordinates before saving. \ No newline at end of file From 22e2b2329caf784ae5f1b403c33705c858321444 Mon Sep 17 00:00:00 2001 From: Johannes Elferich Date: Sun, 30 Mar 2025 21:31:53 -0400 Subject: [PATCH 3/7] Update python versions --- .github/workflows/test_and_deploy.yml | 3 ++- pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 75d2b5e..8076079 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -21,10 +21,11 @@ jobs: matrix: platform: [ ubuntu-latest ] python-version: [ - "3.8", "3.9", "3.10", "3.11", + "3.12", + "3.13", ] steps: diff --git a/pyproject.toml b/pyproject.toml index a25ee4b..8007e0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "hatchling.build" name = "imodmodel" description = "IMOD model files as pandas DataFrames in Python." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = {text = "BSD 3-Clause License"} authors = [ {email = "alisterburt@gmail.com"}, @@ -73,7 +73,7 @@ source = "vcs" # https://github.com/charliermarsh/ruff [tool.ruff] line-length = 88 -target-version = "py38" +target-version = "py39" extend-select = [ "E", # style errors "F", # flakes From febf6a31369cf0981ee8614c883cc7939aad067e Mon Sep 17 00:00:00 2001 From: Johannes Elferich Date: Mon, 31 Mar 2025 13:41:45 -0400 Subject: [PATCH 4/7] Update src/imodmodel/functions.py Co-authored-by: alisterburt --- src/imodmodel/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imodmodel/functions.py b/src/imodmodel/functions.py index 15466be..cf42be9 100644 --- a/src/imodmodel/functions.py +++ b/src/imodmodel/functions.py @@ -24,4 +24,4 @@ def write(dataframe: pd.DataFrame, filename: os.PathLike) -> None: filename : filename to write """ model = ImodModel.from_dataframe(dataframe) - model.to_file(filename) \ No newline at end of file + model.to_file(filename) From 0d2851d343560c02d7d491c32c5738e2913a6263 Mon Sep 17 00:00:00 2001 From: Johannes Elferich Date: Mon, 31 Mar 2025 13:41:51 -0400 Subject: [PATCH 5/7] Update src/imodmodel/models.py Co-authored-by: alisterburt --- src/imodmodel/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imodmodel/models.py b/src/imodmodel/models.py index 874039d..958097c 100644 --- a/src/imodmodel/models.py +++ b/src/imodmodel/models.py @@ -497,7 +497,7 @@ def from_file(cls, filename: os.PathLike): @classmethod def from_dataframe(cls, dataframe: pd.DataFrame, type: ContourType = ContourType.SCATTERED): - """Read an IMOD model from a pandas DataFrame.""" + """Construct an ImodModel instance from a pandas DataFrame.""" # Ensure the DataFrame has the required columns required_columns = ['x', 'y', 'z'] From f0fb24c01f3d53d44d18068dcbe2134bb3df46b1 Mon Sep 17 00:00:00 2001 From: Johannes Elferich Date: Mon, 31 Mar 2025 13:41:57 -0400 Subject: [PATCH 6/7] Update docs/object_api.md Co-authored-by: alisterburt --- docs/object_api.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/object_api.md b/docs/object_api.md index 69109c6..4c3a500 100644 --- a/docs/object_api.md +++ b/docs/object_api.md @@ -1,8 +1,9 @@ # Object-based API -The resulting dataframe from `imodmodel.read()` contains only information about the contours or slicer angles. -The full set of information from the imod model file can be parsed using `ImodModel` +`imodmodel.read()` and `imodmodel.write()` are convenient APIs for accessing information from contours or slicer angles from an IMOD model file. + +`ImodModel` is a [pydantic model](https://docs.pydantic.dev/latest/) for the data in a model file. A more complete set of the information in an IMOD model files can be accessed using the `ImodModel.from_file()` method. ```python from imodmodel import ImodModel From 4e500f8c3da3d40fbdb7ed28e6ec4a20d8b01acd Mon Sep 17 00:00:00 2001 From: Johannes Elferich Date: Fri, 4 Apr 2025 16:10:10 -0400 Subject: [PATCH 7/7] Pass type option to functional API --- src/imodmodel/functions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/imodmodel/functions.py b/src/imodmodel/functions.py index cf42be9..aa500f9 100644 --- a/src/imodmodel/functions.py +++ b/src/imodmodel/functions.py @@ -1,6 +1,6 @@ import os import pandas as pd -from .models import ImodModel +from .models import ImodModel, ContourType from .dataframe import model_to_dataframe @@ -15,7 +15,7 @@ def read(filename: os.PathLike, annotation: str = 'contour') -> pd.DataFrame: model = ImodModel.from_file(filename) return model_to_dataframe(model,annotation) -def write(dataframe: pd.DataFrame, filename: os.PathLike) -> None: +def write(dataframe: pd.DataFrame, filename: os.PathLike, type: ContourType=ContourType.SCATTERED) -> None: """Write a pandas DataFrame to an IMOD model file. Parameters @@ -23,5 +23,5 @@ def write(dataframe: pd.DataFrame, filename: os.PathLike) -> None: dataframe : pandas DataFrame to write filename : filename to write """ - model = ImodModel.from_dataframe(dataframe) + model = ImodModel.from_dataframe(dataframe, type=type) model.to_file(filename)