#!/usr/bin/env python3
# Module from BioBB basics
import argparse
from pathlib import Path
from typing import Optional
from biobb_common.configuration import settings
from biobb_common.generic.biobb_object import BiobbObject
from biobb_common.tools import file_utils as fu
from biobb_common.tools.file_utils import launchlogger
# Modules for BioBB Morph
from biobb_morph.morph import common
[docs]
class Morph(BiobbObject):
"""
| biobb_morph Morph
| The Morph class is designed for the production of 3DSpine meshes, from a template IVD mesh to a target, patient-personalized model.
| The Morph class is designed for the production of 3DSpine meshes, from a template IVD mesh to a target, patient-personalized model. It supports various non-rigid registration modes and morphing operations to achieve optimal mesh alignment and transformation.
Args:
input_AF_stl_path (str): Path to the AF stl input file path. File type: input. `Sample file <https://github.com/bioexcel/biobb_morph/blob/baac08889094d194f898c619ae661510f8cbd498/biobb_morph/test/data/morph/IVD_L2L3_AF_NC0031.stl>`_. Accepted formats: stl (edam:format_3993).
input_NP_stl_path (str): Path to the NP stl input file path. File type: input. `Sample file <https://github.com/bioexcel/biobb_morph/blob/baac08889094d194f898c619ae661510f8cbd498/biobb_morph/test/data/morph/IVD_L2L3_NP_NC0031.stl>`_. Accepted formats: stl (edam:format_3993).
input_lambdaBeta_csv_path (str) (Optional): Path to the csv lambdaBeta input file path. File type: input. `Sample file <https://raw.githubusercontent.com/bioexcel/biobb_morph/refs/heads/master/biobb_morph/sources/lambdaBeta.csv>`_. Accepted formats: csv (edam:format_3752).
output_morphed_zip_path (str): Path to the output morphed zip file path. File type: output. `Sample file <https://urlto.sample>`_. Accepted formats: zip (edam:format_3752).
properties (dict - Python dictionary object containing the tool parameters, not input/output files):
* **morph** (*int*) - (5) Non-Rigid registration mode. Options: 1: AF, 2: NP, 3: NoBEP, 4: CEPmorph, 5: All, 0: NONE.
* **toINP** (*int*) - (4) Create the .inp file for specific components. Options: 1: AF, 2: NP, 3: NoBEP, 4: All, 0: NONE.
* **abaqusCommand** (*str*) - ("gmsh") Command used to call ABAQUS. If '-a gmsh', the Gmsh tool is used.
* **bcpdCommand** (*str*) - ("bcpd") Command used to call BCPD++.
* **checkFElem** (*int*) - (2) Check failed elements of the resulting .inp file (Abaqus required). Options: 1: YES, 2: Iterate value of lambda, 0: NO.
* **rigid** (*int*) - (1) Perform rigid registration at the beginning of the process. Options: 1: YES, 0: NO.
* **WCEP** (*int*) - (0) Perform morphing with CEP. Options: 1: YES, 0: NO.
* **interpo** (*int*) - (1) Use interpolated files. Options: 1: YES, 0: NO.
* **fusion** (*int*) - (1) Fuse the AF and NP for the final morph. Options: 1: YES, 0: NO.
* **surfRegCEP** (*int*) - (1) Morph external surfaces of AF and NP (including CEP). Options: 1: YES, 0: NO.
* **checkHaus** (*int*) - (1) Check Hausdorff distance between 3D grids (Euclidean distance). Options: 1: YES, 0: NO.
* **CEP** (*int*) - (0) Perform non-rigid registration of the CEP. Options: 1: YES, 0: NO.
* **TZ** (*int*) - (1) Create a Transition Zone. Options: 1: YES, 0: NO.
* **movement** (*list*) - ([0, 0, 0.05]) Enter a list of floats separated by spaces to represent desired movement. Positive: positive direction, Negative: negative direction, 0: no movement.
* **nodeDistance** (*float*) - (0.3) Distance between two nodes of the mesh.
* **moveTo** (*list*) - ([0.0, 24.1397991, 2.94929004]) Translation of the AF and NP.
* **plane** (*list*) - ([1, 1, 0]) Plane to orthogonally project the nodes of the NP to create the spline line of the perimeter.
* **reduce_param** (*float*) - (0.8) Parameter to reduce the size of the contour of the NP.
Examples:
This is a use example of how to use the building block from Python::
from biobb_morph.morph.morph import morph
prop = {
'morph: 5
}
morph(input_AF_stl_path='/path/to/AF.stl',
input_NP_stl_path='/path/to/NP.stl',
output_morphd_zip_path='/path/to/morphed.zip',
properties=prop)
Info:
* wrapped_software:
* name: Morph
* version: >=1.0.0
* license: BSD 3-Clause
* ontology:
* name: EDAM
* schema: http://edamontology.org/EDAM.owl
"""
def __init__(
self,
input_AF_stl_path: str,
input_NP_stl_path: str,
output_morphed_zip_path: str,
input_lambdaBeta_csv_path: Optional[str] = None,
properties: Optional[dict] = None,
**kwargs,
) -> None:
properties = properties or {}
# Call parent class constructor
super().__init__(properties)
self.locals_var_dict = locals().copy()
self.sources_path = str(Path(__file__).resolve().parents[1].joinpath("sources"))
input_lambdaBeta_csv_path = input_lambdaBeta_csv_path or str(
Path(self.sources_path).joinpath("lambdaBeta.csv")
)
# Input/Output files
self.io_dict = {
"in": {
"input_AF_stl_path": input_AF_stl_path,
"input_NP_stl_path": input_NP_stl_path,
"input_lambdaBeta_csv_path": input_lambdaBeta_csv_path,
},
"out": {
"output_morphed_zip_path": output_morphed_zip_path,
},
}
# Properties
# WARNING: Not a property but a file it should be in the input
# self.files = self.io_dict["in"]["input_file_path1"]
self.morph = properties.get("morph", 5)
self.toINP = properties.get("toINP", 4)
self.abaqusCommand = properties.get("abaqusCommand", "gmsh")
self.bcpdCommand = properties.get("bcpdCommand", "bcpd")
self.checkFElem = properties.get("checkFElem", 2)
self.rigid = properties.get("rigid", 1)
self.WCEP = properties.get("WCEP", 0)
self.interpo = properties.get("interpo", 1)
self.fusion = properties.get("fusion", 1)
self.surfRegCEP = properties.get("surfRegCEP", 1)
self.checkHaus = properties.get("checkHaus", 1)
self.CEP = properties.get("CEP", 0)
# WARNING: Not a property but a file it should be in the input
# self.lambdaBeta = properties.get("lambdaBeta", "lambdaBeta.csv")
self.TZ = properties.get("TZ", 1)
self.movement = properties.get("movement", [0, 0, 0.05])
self.nodeDistance = properties.get("nodeDistance", 0.3)
self.moveTo = properties.get("moveTo", [0.0, 24.1397991, 2.94929004])
self.plane = properties.get("plane", [1, 1, 0])
self.reduce_param = properties.get("reduce_param", 0.8)
# Check the properties
self.check_properties(properties)
self.check_arguments()
[docs]
@launchlogger
def launch(self) -> int:
"""Execute the :class:`Morph <biobb_morph.biobb_morph.Morph>` object."""
if self.check_restart():
return 0
self.stage_files()
fu.log(
f"input_AF_stl_path: {self.stage_io_dict['in']['input_AF_stl_path']}",
self.out_log,
)
fu.log(
f"input_NP_stl_path: {self.stage_io_dict['in']['input_NP_stl_path']}",
self.out_log,
)
# Create input_stl_path
input_stl_path = Path(
self.stage_io_dict["in"]["input_AF_stl_path"]
).parent.joinpath("input.stl")
with open(input_stl_path, "w") as f:
f.write(f"{Path(self.stage_io_dict['in']['input_AF_stl_path']).name}\n")
f.write(f"{Path(self.stage_io_dict['in']['input_NP_stl_path']).name}")
fu.log(
f"input.stl: {input_stl_path}",
self.out_log,
)
# Create results directory
results_path = Path(
self.stage_io_dict["out"]["output_morphed_zip_path"]
).parent.joinpath("results")
fu.log(
f"results_path: {results_path}",
self.out_log,
)
# Adding specific morphing properties
morph_args = [
str(input_stl_path),
str(self.sources_path),
str(results_path),
self.morph,
self.WCEP,
self.toINP,
self.interpo,
self.fusion,
self.rigid,
self.surfRegCEP,
self.checkFElem,
self.checkHaus,
self.CEP,
self.nodeDistance,
self.moveTo,
self.movement,
self.plane,
self.reduce_param,
self.TZ,
self.stage_io_dict["in"]["input_lambdaBeta_csv_path"],
self.bcpdCommand,
self.abaqusCommand,
]
# Executing the command line as a list of items (elements order will be maintained)
fu.log("Executing the morph function with specified arguments", self.out_log)
common.morph(*morph_args)
# Create results zip file
fu.log(
f"results zip file: {self.stage_io_dict['out']['output_morphed_zip_path']}",
self.out_log,
)
fu.zip_list(
self.stage_io_dict["out"]["output_morphed_zip_path"], [results_path]
)
# Copy files to host
self.copy_to_host()
# Remove temporal files
self.remove_tmp_files()
self.check_arguments(output_files_created=True, raise_exception=False)
return 0
[docs]
def morph(
input_AF_stl_path: str,
input_NP_stl_path: str,
output_morphed_zip_path: str,
input_lambdaBeta_csv_path: Optional[str] = None,
properties: Optional[dict] = None,
**kwargs,
) -> int:
"""Create :class:`Morph <biobb_morph.biobb_morph.Morph>` class and
execute the :meth:`launch() <biobb_morph.biobb_morph.Morph.launch>` method."""
return Morph(
input_AF_stl_path=input_AF_stl_path,
input_NP_stl_path=input_NP_stl_path,
output_morphed_zip_path=output_morphed_zip_path,
input_lambdaBeta_csv_path=input_lambdaBeta_csv_path,
properties=properties,
**kwargs,
).launch()
[docs]
def main():
parser = argparse.ArgumentParser(
description="Wrapper of the Morph module.",
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, width=99999),
)
parser.add_argument(
"-c",
"--config",
required=False,
help="This file can be a YAML file, JSON file or JSON string",
)
# Specific args
required_args = parser.add_argument_group("required arguments")
required_args.add_argument(
"--input_AF_stl_path",
required=True,
help="Input AF stl file path",
)
required_args.add_argument(
"--input_NP_stl_path",
required=True,
help="Input NP stl file path",
)
required_args.add_argument(
"--output_morphed_zip_path",
required=True,
help="Output morphed zip file path",
)
parser.add_argument(
"--input_lambdaBeta_csv_path",
required=False,
help="Input lambdaBeta csv file path",
)
args = parser.parse_args()
config = args.config if args.config else None
properties = settings.ConfReader(config=config).get_prop_dic()
morph(
input_AF_stl_path=args.input_AF_stl_path,
input_NP_stl_path=args.input_NP_stl_path,
output_morphed_zip_path=args.output_morphed_zip_path,
input_lambdaBeta_csv_path=args.input_lambdaBeta_csv_path,
properties=properties,
)
if __name__ == "__main__":
main()