From 5e8b49fb747afa7a6d7bee1feddeb6fd208eddd1 Mon Sep 17 00:00:00 2001 From: "Donal K. Fellows" Date: Wed, 5 Aug 2026 14:11:26 +0100 Subject: [PATCH] Fixing minor problems, starting to fix types --- cipher_parse/cipher_input.py | 109 +++++++++++++----------- cipher_parse/cipher_output.py | 11 +-- cipher_parse/discrete_voronoi.py | 5 +- cipher_parse/geometry.py | 140 +++++++++++++++++-------------- cipher_parse/interface.py | 32 +++---- cipher_parse/material.py | 20 +++-- cipher_parse/quats.py | 10 +-- cipher_parse/utilities.py | 7 +- cipher_parse/voxel_map.py | 80 +++++++++--------- 9 files changed, 220 insertions(+), 194 deletions(-) diff --git a/cipher_parse/cipher_input.py b/cipher_parse/cipher_input.py index 9639dbc..505f9f8 100644 --- a/cipher_parse/cipher_input.py +++ b/cipher_parse/cipher_input.py @@ -3,9 +3,9 @@ from pathlib import Path from dataclasses import dataclass from textwrap import indent -from typing import Optional, List, Union, Tuple, Dict import numpy as np +from numpy.typing import NDArray import h5py from parse import parse from ruamel.yaml import YAML @@ -14,12 +14,13 @@ from cipher_parse.geometry import CIPHERGeometry from cipher_parse.interface import InterfaceDefinition from cipher_parse.material import MaterialDefinition -from cipher_parse.utilities import set_by_path, read_shockley, grain_boundary_mobility +from cipher_parse.utilities import ( + set_by_path, read_shockley, grain_boundary_mobility) -def compress_1D_array(arr): - vals = [] - nums = [] +def compress_1D_array(arr: NDArray) -> tuple[list[int], list[int]]: + vals: list[int] = [] + nums: list[int] = [] for idx, i in enumerate(arr): if idx == 0: vals.append(i) @@ -37,7 +38,7 @@ def compress_1D_array(arr): return nums, vals -def compress_1D_array_string(arr, item_delim="\n"): +def compress_1D_array_string(arr: NDArray, item_delim="\n"): out = [] for n, v in zip(*compress_1D_array(arr)): out.append(f"{n} of {v}" if n > 1 else f"{v}") @@ -45,8 +46,8 @@ def compress_1D_array_string(arr, item_delim="\n"): return item_delim.join(out) -def decompress_1D_array_string(arr_str, item_delim="\n"): - out = [] +def decompress_1D_array_string(arr_str: str, item_delim="\n") -> NDArray: + out: list[int] = [] for i in arr_str.split(item_delim): if not i: continue @@ -62,10 +63,10 @@ def decompress_1D_array_string(arr_str, item_delim="\n"): @dataclass class CIPHERInput: geometry: CIPHERGeometry - components: List - outputs: List - solution_parameters: Dict - quiet: Optional[bool] = False + components: list + outputs: list + solution_parameters: dict + quiet: bool | None = False def __post_init__(self): self._validate() @@ -89,11 +90,13 @@ def _validate(self): ) if not np.all(check_grid_size == np.array(self.geometry.grid_size)): raise ValueError( - f"`grid_size` (specifed: {self.geometry.grid_size}) must be equal to: " - f"`initblocksize` (specified: {self.solution_parameters['initblocksize']}) " - f"multiplied by 2 raised to the power of `initrefine` (specified: " - f"{self.solution_parameters['initrefine']}), calculated to be: " - f"{check_grid_size}." + f"`grid_size` " + f"(specifed: {self.geometry.grid_size}) " + f"must be equal to: `initblocksize` " + f"(specified: {self.solution_parameters['initblocksize']}) " + f"multiplied by 2 raised to the power of `initrefine` " + f"(specified: {self.solution_parameters['initrefine']}), " + f"calculated to be: {check_grid_size}." ) def to_JSON_file(self, path): @@ -175,7 +178,7 @@ def get_input_maps_from_files( return (voxel_phase, phase_material, interface_map) @classmethod - def from_input_YAML_file(cls, path): + def from_input_YAML_file(cls, path: Path): """Generate a CIPHERInput object from a CIPHER input YAML file.""" with Path(path).open("rt") as fp: @@ -198,14 +201,14 @@ def from_input_YAML_file(cls, path): ) @classmethod - def read_input_YAML_file(cls, path): + def read_input_YAML_file(cls, path: str | Path): with Path(path).open("rt") as fp: file_str = "".join(fp.readlines()) return cls.read_input_YAML_string(file_str=file_str) @staticmethod - def read_input_YAML_string(file_str, parse_interface_map=True): + def read_input_YAML_string(file_str: str, parse_interface_map=True): yaml = YAML(typ="safe") data = yaml.load(file_str) @@ -256,7 +259,7 @@ def read_input_YAML_string(file_str, parse_interface_map=True): @classmethod def from_input_YAML_str( cls, - file_str, + file_str: str, input_map_voxel_phase=None, input_map_phase_material=None, input_map_interface=None, @@ -283,7 +286,7 @@ def from_input_YAML_str( ) for idx, (name, props) in enumerate(yaml_dat["material"].items()) ] - interfaces = [] + interfaces: list[InterfaceDefinition] = [] for idx, (int_name, props) in enumerate(yaml_dat["interface"].items()): phase_pairs = np.vstack(np.where(yaml_dat["interface_map"] == idx)).T if phase_pairs.size: @@ -325,14 +328,14 @@ def from_voronoi( grid_size, size, materials, - interfaces, + interfaces: list[InterfaceDefinition], components, outputs, solution_parameters, seeds=None, num_phases=None, random_seed=None, - is_periodic=False, + is_periodic: bool = False, combine_phases=None, ): geometry = CIPHERGeometry.from_voronoi( @@ -362,7 +365,7 @@ def from_seed_voronoi( grid_size, size, materials, - interfaces, + interfaces: list[InterfaceDefinition], components, outputs, solution_parameters, @@ -391,7 +394,7 @@ def from_random_voronoi( grid_size, size, materials, - interfaces, + interfaces: list[InterfaceDefinition], components, outputs, solution_parameters, @@ -419,7 +422,7 @@ def from_voxel_phase_map( voxel_phase, size, materials, - interfaces, + interfaces: list[InterfaceDefinition], components, outputs, solution_parameters, @@ -447,7 +450,7 @@ def from_dream3D( cls, path, materials, - interfaces, + interfaces: list[InterfaceDefinition], components, outputs, solution_parameters, @@ -576,15 +579,15 @@ def materials(self): return self.geometry.materials @property - def material_properties(self): + def material_properties(self) -> dict: return self.geometry.material_properties @property - def interfaces(self): + def interfaces(self) -> list[InterfaceDefinition]: return self.geometry.interfaces @property - def interface_names(self): + def interface_names(self) -> list[str]: return self.geometry.interface_names def get_header(self): @@ -599,10 +602,10 @@ def get_header(self): } return out - def get_interfaces(self): + def get_interfaces(self) -> dict[str, dict]: return {i.name: i.properties for i in self.geometry.interfaces} - def write_yaml(self, path, separate_mappings=False): + def write_yaml(self, path: str, separate_mappings: bool = False) -> Path: """Write the CIPHER input YAML file. Parameters @@ -659,22 +662,22 @@ def write_yaml(self, path, separate_mappings=False): } yaml = YAML() - path = Path(path) - with path.open("wt", newline="\n") as fp: + path_ = Path(path) + with path_.open("wt", newline="\n") as fp: yaml.dump(cipher_input_data, fp) - return path + return path_ def bin_interfaces_by_misorientation_angle( self, - base_interface_name, + base_interface_name: str, theta_max, energy_range=None, mobility_range=None, - n=4, - B=5, - bin_width=5, - degrees=True, + n: int = 4, + B: int = 5, + bin_width: int = 5, + degrees: bool = True, **kwargs, ): if energy_range is None and mobility_range is None: @@ -795,8 +798,8 @@ def bin_interfaces_by_misorientation_angle( def apply_interface_property( self, - base_interface_name, - property_name, + base_interface_name: str, + property_name: str | list[str] | tuple[str, ...], property_values, additional_metadata=None, bin_edges=None, @@ -818,8 +821,10 @@ def apply_interface_property( """ - if not isinstance(property_name, list): - property_name = [property_name] + if isinstance(property_name, list): + property_name = tuple(property_name) + elif isinstance(property_name, str): + property_name = (property_name, ) if not isinstance(property_values, list): property_values = [property_values] @@ -843,8 +848,9 @@ def apply_interface_property( else: value = bin_i print( - f"Adding {pp_idx_i.size!r} phase pair(s) to {property_name!r} bin " - f"{idx + 1} with edge value: {bin_i!r} and centre: {value!r}." + f"Adding {pp_idx_i.size!r} phase pair(s) to " + f"{property_name!r} bin {idx + 1} with edge " + f"value: {bin_i!r} and centre: {value!r}." ) new_interfaces_data.append( { @@ -879,9 +885,10 @@ def apply_interface_property( ) ) raise RuntimeError( - f"Not all phase pairs have been added to a property value bin. The " - f"following {len(missing_dat)}/{phase_pairs.shape[1]} phase pairs (and " - f"property values) are missing: {missing_dat}." + f"Not all phase pairs have been added to a property " + f"value bin. The following " + f"{len(missing_dat)}/{phase_pairs.shape[1]} " + f"phase pairs (and property values) are missing: {missing_dat}." ) else: print( @@ -900,7 +907,7 @@ def apply_interface_property( for idx, i in enumerate(new_interfaces_data): props = copy.deepcopy(base_defn.properties) for name, val in zip(property_name, i["values"]): - new_value = val.item() # convert from numpy to native + new_value = val.item() # convert from numpy to native set_by_path(root=props, path=name, value=new_value) new_type_lab = str(idx) diff --git a/cipher_parse/cipher_output.py b/cipher_parse/cipher_output.py index 86aa1f2..526f6eb 100644 --- a/cipher_parse/cipher_output.py +++ b/cipher_parse/cipher_output.py @@ -1,11 +1,6 @@ -import copy import json -import shutil -from subprocess import run, PIPE from pathlib import Path import re -import os -from textwrap import dedent import numpy as np import pyvista as pv @@ -13,7 +8,7 @@ import plotly.express as px import zarr -from cipher_parse.cipher_input import CIPHERInput, decompress_1D_array_string +from cipher_parse.cipher_input import CIPHERInput from cipher_parse.geometry import CIPHERGeometry from cipher_parse.utilities import ( get_subset_indices, @@ -753,7 +748,7 @@ def _prepare_phase_size_dist_evolution_dataframe( max_phase_size = df.phase_size.max() if num_bins is not None and bin_size is not None: - raise TypeError(f"Specify exactly one of `num_bins` and `bin_size`.") + raise TypeError("Specify exactly one of `num_bins` and `bin_size`.") elif num_bins is None and bin_size is None: num_bins = 50 @@ -933,7 +928,7 @@ def show_misorientation_dist_evolution( max_misori = max_misori_i if num_bins is not None and bin_size is not None: - raise TypeError(f"Specify exactly one of `num_bins` and `bin_size`.") + raise TypeError("Specify exactly one of `num_bins` and `bin_size`.") elif num_bins is None and bin_size is None: num_bins = 50 diff --git a/cipher_parse/discrete_voronoi.py b/cipher_parse/discrete_voronoi.py index 511067d..1815504 100644 --- a/cipher_parse/discrete_voronoi.py +++ b/cipher_parse/discrete_voronoi.py @@ -39,8 +39,8 @@ def __init__( Parameters ---------- region_seeds : list or ndarray of shape (N, 2) or (N, 3) - Row vectors of seed positions in 2D or 3D. Must be coordinates within real-space - `size`. + Row vectors of seed positions in 2D or 3D. + Must be coordinates within real-space `size`. grid_size : list or ndarray of length 2 or 3 size : list or ndarray of length 2 or 3, optional If not specified, a unit square/box is used. @@ -133,6 +133,7 @@ def get_unique_random_seeds(cls, num_regions, size, grid_size, random_seed=None) """Get random seeds that occupy unique elements on the voxel grid.""" max_search_iter = 10_000 idx = 0 + counts = np.array([0]) while idx == 0 or np.any(counts > 1): random_seed = random_seed + idx if random_seed else None seeds = cls.get_random_seeds(num_regions, size, random_seed) diff --git a/cipher_parse/geometry.py b/cipher_parse/geometry.py index 180c982..f461c0e 100644 --- a/cipher_parse/geometry.py +++ b/cipher_parse/geometry.py @@ -4,6 +4,7 @@ from damask import Orientation import pyvista as pv import numpy as np +from numpy.typing import NDArray import plotly.express as px from cipher_parse.material import MaterialDefinition @@ -29,16 +30,16 @@ class CIPHERGeometry: def __init__( self, - materials, - interfaces, + materials: list[MaterialDefinition], + interfaces: list[InterfaceDefinition], size, - seeds=None, - voxel_phase=None, - voxel_map=None, - is_periodic=False, - random_seed=None, - allow_missing_phases=False, - quiet=False, + seeds: list | None = None, + voxel_phase: NDArray | None = None, + voxel_map: VoxelMap | None = None, + is_periodic: bool = False, + random_seed: int | None = None, + allow_missing_phases: bool = False, + quiet: bool = False, time=None, increment=None, incremental_data_idx=None, @@ -54,7 +55,7 @@ def __init__( """ if sum(i is not None for i in (voxel_phase, voxel_map)) != 1: - raise ValueError(f"Specify exactly one of `voxel_phase` and `voxel_map`") + raise ValueError("Specify exactly one of `voxel_phase` and `voxel_map`") if voxel_map is None: voxel_map = VoxelMap( region_ID=voxel_phase, @@ -99,8 +100,8 @@ def __init__( if not allow_missing_phases: if not np.all(all_phases == np.arange(self.num_phases)): raise GeometryVoxelPhaseError( - "`voxel_phase` must be an array of consecutive integers starting from " - "zero." + "`voxel_phase` must be an array of consecutive integers starting " + "from zero." ) if len(set(self.material_names)) < self.num_materials: @@ -152,7 +153,10 @@ def __init__( self._misorientation_matrix_is_degrees = None @staticmethod - def combine_phases_per_phase_type(voxel_map, materials, combine_phases): + def combine_phases_per_phase_type( + voxel_map: VoxelMap, materials: list[MaterialDefinition], + combine_phases: dict + ) -> NDArray: print(f"combining phases according to {combine_phases}") @@ -198,7 +202,8 @@ def combine_phases_per_phase_type(voxel_map, materials, combine_phases): sampled_ID = random.sample(possible_IDs_i, 1)[0] except ValueError: print( - f"No non-neighbouring samples left for root_ID: {root_ID}." + f"No non-neighbouring samples left for " + f"root_ID: {root_ID}." ) # allow touching phase IDs within this group: possible_IDs_i = possible_IDs - set(shared_IDs) @@ -207,7 +212,7 @@ def combine_phases_per_phase_type(voxel_map, materials, combine_phases): sampled_ID = random.sample(possible_IDs_i, 1)[0] else: raise ValueError( - f"Cannot find non-neighbouring root IDs" + "Cannot find non-neighbouring root IDs" ) from None neighbours_sampled = set( @@ -227,7 +232,8 @@ def combine_phases_per_phase_type(voxel_map, materials, combine_phases): pt_i.phases = kept_IDs # modify phase type phases - # reindex phases across all materials to maintain consecutive phase IDs: + # reindex phases across all materials to maintain consecutive + # phase IDs: voxel_phase_new_flat = voxel_phase_new.reshape(-1) uniq, inv = np.unique(voxel_phase_new_flat, return_inverse=True) reindex = dict(zip(uniq, range(len(uniq)))) @@ -256,11 +262,11 @@ def _validate_interfaces(self): int_names = self.interface_names if len(set(int_names)) < len(int_names): raise ValueError( - f"Multiple interfaces have the same name (i.e. " - f"phase-type-pair and type-label combination)!" + "Multiple interfaces have the same name (i.e. " + "phase-type-pair and type-label combination)!" ) - def to_JSON(self, keep_arrays=False): + def to_JSON(self, keep_arrays: bool = False) -> dict: data = { "materials": [i.to_JSON(keep_arrays) for i in self.materials], "interfaces": [i.to_JSON(keep_arrays) for i in self.interfaces], @@ -295,7 +301,7 @@ def to_JSON(self, keep_arrays=False): return data @classmethod - def from_JSON(cls, data, quiet=True): + def from_JSON(cls, data: dict, quiet: bool = True): data_init = { "materials": [MaterialDefinition.from_JSON(i) for i in data["materials"]], "interfaces": [InterfaceDefinition.from_JSON(i) for i in data["interfaces"]], @@ -340,32 +346,32 @@ def known_phases(self): return np.concatenate(phases) @property - def interfaces(self): + def interfaces(self) -> list[InterfaceDefinition]: return self._interfaces @property - def is_periodic(self): + def is_periodic(self) -> bool: return self._is_periodic @interfaces.setter - def interfaces(self, interfaces): + def interfaces(self, interfaces: list[InterfaceDefinition]): self._interfaces = interfaces self._validate_interfaces() @property - def misorientation_matrix(self): + def misorientation_matrix(self) -> NDArray | None: return self._misorientation_matrix @property def misorientation_matrix_is_degrees(self): return self._misorientation_matrix_is_degrees - def get_phase_voxels(self): + def get_phase_voxels(self) -> list: if self._phase_voxels is None: self._calculate_phase_voxels() return self._phase_voxels - def get_phase_num_voxels(self): + def get_phase_num_voxels(self) -> NDArray: if self._phase_num_voxels is None: self._calculate_phase_num_voxels() return self._phase_num_voxels @@ -425,7 +431,7 @@ def _calculate_grain_boundaries(self): calc_count = 0 report_each_pc = 5 num_iter_per_report = np.ceil(tot_num_calcs * report_each_pc / 100) - print(f"Identifying grain boundaries...", flush=True) + print("Identifying grain boundaries...", flush=True) for int_idx, interface in enumerate(self.interfaces): for phase_pair in interface.phase_pairs: calc_count += 1 @@ -461,7 +467,7 @@ def _calculate_grain_boundaries(self): "voxel_coordinates": vox_coords, "centroid": GB_centroid, } - print(f"Finished grain boundaries.", flush=True) + print("Finished grain boundaries.", flush=True) self._grain_boundaries = grain_boundaries def _calculate_grain_boundary_centroids(self): @@ -469,17 +475,17 @@ def _calculate_grain_boundary_centroids(self): [i["centroid"][None] for i in self.get_grain_boundaries().values()], axis=0 ) - def _ensure_phase_assignment(self, random_seed): + def _ensure_phase_assignment(self, random_seed: int | None): is_mat_phases = [i.phases is not None for i in self.materials] is_mat_vol_frac = [i is not None for i in self.target_material_volume_fractions] is_mixed = any(is_mat_phases) and any(is_mat_vol_frac) if is_mixed or (any(is_mat_phases) and not all(is_mat_phases)): raise GeometryMissingPhaseAssignmentError( - f"Specify either: all phases explicitly (via the material definition " - f"`phases`, or the constituent phase type definition `phases`), or " - f"specify zero or more target volume fractions for the material " - f"definitions." + "Specify either: all phases explicitly (via the material definition " + "`phases`, or the constituent phase type definition `phases`), or " + "specify zero or more target volume fractions for the material " + "definitions." ) if not any(is_mat_phases): @@ -511,7 +517,8 @@ def _check_interface_phase_pairs(self): f"{i.materials[0]!r} and {i.materials[1]!r}." ) # TODO: test raise - def _assign_phases_by_volume_fractions(self, is_mat_vol_frac, random_seed): + def _assign_phases_by_volume_fractions( + self, is_mat_vol_frac, random_seed: int | None): # Assign via target volume fractions. num_unassigned_vol = self.num_materials - sum(is_mat_vol_frac) assigned_vol = sum(i or 0.0 for i in self.target_material_volume_fractions) @@ -594,8 +601,10 @@ def _get_phase_orientation(self): return phase_ori def get_interface_map_indices(self, phase_type_A, phase_type_B): - """Get an array of integer indices that index the (upper triangle of the) 2D - symmetric interface map array, corresponding to a given material pair.""" + """ + Get an array of integer indices that index the (upper triangle of the) 2D + symmetric interface map array, corresponding to a given material pair. + """ # First get phase indices belonging to the two phase types: ptypes = {i.name: i for i in self.phase_types} @@ -615,8 +624,10 @@ def get_interface_map_indices(self, phase_type_A, phase_type_B): return map_idx_non_trivial def _get_interface_map(self, upper_tri_only=False, quiet=False): - """Generate the num_phases by num_phases symmetric matrix that maps each phase-pair - to an interface index.""" + """ + Generate the num_phases by num_phases symmetric matrix that maps each phase-pair + to an interface index. + """ if not quiet: print("Finding interface map matrix...", end="") @@ -646,18 +657,20 @@ def _get_interface_map(self, upper_tri_only=False, quiet=False): if any_frac_set: if any_manual_set: raise ValueError( - f"For interface {pt_pair}, specify phase pairs manually for all " - f"defined interfaces using `phase_pairs`, or specify `type_fraction`" - f"for all defined interfaces. You cannot mix them." + f"For interface {pt_pair}, specify phase pairs manually for " + f"all defined interfaces using `phase_pairs`, or specify " + f"`type_fraction` for all defined interfaces. " + f"You cannot mix them." ) all_phase_pairs = self.get_interface_map_indices(*pt_pair).T if any_manual_set: if not all_manual_set: raise ValueError( - f"For interface {pt_pair}, specify phase pairs manually for all " - f"defined interfaces using `phase_pairs`, or specify `type_fraction`" - f"for all defined interfaces. You cannot mix them." + f"For interface {pt_pair}, specify phase pairs manually for " + f"all defined interfaces using `phase_pairs`, or specify " + f"`type_fraction` for all defined interfaces. " + f"You cannot mix them." ) # check that given phase_pairs combine to the set of all phase_pairs @@ -808,7 +821,8 @@ def get_pyvista_grid(self): return grid @staticmethod - def get_unique_random_seeds(num_phases, size, grid_size, random_seed=None): + def get_unique_random_seeds( + num_phases: int, size, grid_size, random_seed: int | None = None) -> NDArray: return DiscreteVoronoi.get_unique_random_seeds( num_regions=num_phases, size=size, @@ -839,18 +853,18 @@ def assign_phase_material_randomly( @classmethod def from_voronoi( cls, - interfaces, - materials, + interfaces: list[InterfaceDefinition], + materials: list[MaterialDefinition], grid_size, size, - seeds=None, + seeds: list | None = None, num_phases=None, - random_seed=None, + random_seed: int | None = None, is_periodic=False, combine_phases=None, ): if sum(i is not None for i in (seeds, num_phases)) != 1: - raise ValueError(f"Specify exactly one of `seeds` and `num_phases`") + raise ValueError("Specify exactly one of `seeds` and `num_phases`") if seeds is None: vor_map = DiscreteVoronoi.from_random( @@ -883,12 +897,12 @@ def from_voronoi( @classmethod def from_seed_voronoi( cls, - seeds, - interfaces, - materials, + seeds: list, + interfaces: list[InterfaceDefinition], + materials: list[MaterialDefinition], grid_size, size, - random_seed=None, + random_seed: int | None = None, is_periodic=False, ): return cls.from_voronoi( @@ -905,11 +919,11 @@ def from_seed_voronoi( def from_random_voronoi( cls, num_phases, - interfaces, - materials, + interfaces: list[InterfaceDefinition], + materials: list[MaterialDefinition], grid_size, size, - random_seed=None, + random_seed: int | None = None, is_periodic=False, ): return cls.from_voronoi( @@ -1275,9 +1289,11 @@ def get_voxel_IPF(self, IPF_dir=None, as_3D=False): else: return vox_IPF - def remove_interface(self, interface_name): - """Remove an interface from the geometry. This will invalidate the geometry if - the specified interface is referred by any phase-pairs.""" + def remove_interface(self, interface_name: str): + """ + Remove an interface from the geometry. This will invalidate the geometry if + the specified interface is referred by any phase-pairs. + """ idx = self.interface_names.index(interface_name) interface = self.interfaces.pop(idx) @@ -1377,8 +1393,8 @@ def show_interface_energies_by_misorientation( x.append(m_i) color.append(bin_idx) hover.append( - f"({bin_i['phase_pairs'][m_i_idx, 0], bin_i['phase_pairs'][m_i_idx, 1]})" - ) + f"({bin_i['phase_pairs'][m_i_idx, 0], + bin_i['phase_pairs'][m_i_idx, 1]})") fig.add_scatter( x=x, diff --git a/cipher_parse/interface.py b/cipher_parse/interface.py index 4f71a80..a60ecff 100644 --- a/cipher_parse/interface.py +++ b/cipher_parse/interface.py @@ -1,6 +1,4 @@ import copy -from typing import Dict, List, Optional, Tuple, Union - import numpy as np @@ -9,12 +7,14 @@ class InterfaceDefinition: Attributes ---------- materials : - Between which named materials this interface applies. Specify this or `phase_types`. + Between which named materials this interface applies. + Specify this or `phase_types`. phase_types : - Between which named phase types this interface applies. Specify this or `materials`. + Between which named phase types this interface applies. + Specify this or `materials`. type_label : - To distinguish between multiple interfaces that all apply between the same pair of - materials + To distinguish between multiple interfaces that all apply between + the same pair of materials phase_pairs : List of phase pair indices that should have this interface type (for manual specification). Can be specified as an (N, 2) array. @@ -22,13 +22,13 @@ class InterfaceDefinition: def __init__( self, - properties: Dict, - materials: Optional[Union[List[str], Tuple[str]]] = None, - phase_types: Optional[Union[List[str], Tuple[str]]] = None, - type_label: Optional[str] = None, - type_fraction: Optional[float] = None, - phase_pairs: Optional[np.ndarray] = None, - metadata: Optional[Dict] = None, + properties: dict, + materials: list[str] | tuple[str, ...] | None = None, + phase_types: list[str] | tuple[str, ...] | None = None, + type_label: str | None = None, + type_fraction: float | None = None, + phase_pairs: np.ndarray | None = None, + metadata: dict | None = None, ): self._is_phase_pairs_set = False self.index = None # assigned by parent CIPHERGeometry @@ -142,9 +142,9 @@ def metadata(self, metadata): for k, v in metadata.items(): if len(v) != self.num_phase_pairs: raise ValueError( - f"Item {k!r} in the `metadata` dict must have length equal to the " - f"number of phase pairs ({self.num_phase_pairs}) but has length: " - f"{len(v)}." + f"Item {k!r} in the `metadata` dict must have length equal " + f"to the number of phase pairs ({self.num_phase_pairs}) but " + f"has length: {len(v)}." ) self._metadata = metadata diff --git a/cipher_parse/material.py b/cipher_parse/material.py index 4218815..3320b6b 100644 --- a/cipher_parse/material.py +++ b/cipher_parse/material.py @@ -1,5 +1,5 @@ import numpy as np - +from numpy.typing import NDArray from cipher_parse.errors import ( MaterialPhaseTypeFractionError, MaterialPhaseTypeLabelError, @@ -115,8 +115,8 @@ def __init__( for i in phase_types or []: if i.phases is not None: raise ValueError( - f"Cannot specify `phases` in any of the phase type definitions if " - f"`phases` is also specified in the material definition." + "Cannot specify `phases` in any of the phase type definitions " + "if `phases` is also specified in the material definition." ) # TODO: test raise else: if phase_types: @@ -134,8 +134,8 @@ def __init__( pt_labels = [i.type_label for i in phase_types] if len(set(pt_labels)) < len(pt_labels): raise MaterialPhaseTypeLabelError( - f"Phase types belonging to the same material ({self.name!r}) must have " - f"distinct `type_label`s." + f"Phase types belonging to the same material ({self.name!r}) must " + f"have distinct `type_label`s." ) self.phase_types = phase_types @@ -217,7 +217,7 @@ def target_phase_type_fractions(self): return [i.target_type_fraction for i in self.phase_types] @property - def phases(self): + def phases(self) -> NDArray | None: try: return np.concatenate([i.phases for i in self.phase_types]) except ValueError: @@ -240,7 +240,9 @@ def phase_type_fractions(self): return np.array(phase_type_fractions) def assign_phases(self, phases, random_seed=None): - """Assign given phase indices to phase types according to target_type_fractions.""" + """ + Assign given phase indices to phase types according to target_type_fractions. + """ phases = np.asarray(phases) @@ -260,8 +262,8 @@ def assign_phases(self, phases, random_seed=None): num_phases_i = len(phase_idx_i) if num_oris_i < num_phases_i: raise ValueError( - f"Insufficient number of orientations ({num_oris_i}) for phase type " - f"{type_idx} with {num_phases_i} phases." + f"Insufficient number of orientations ({num_oris_i}) for " + f"phase type {type_idx} with {num_phases_i} phases." ) elif num_oris_i > num_phases_i: # select a subset randomly: diff --git a/cipher_parse/quats.py b/cipher_parse/quats.py index 4a4bce2..20029d7 100644 --- a/cipher_parse/quats.py +++ b/cipher_parse/quats.py @@ -69,7 +69,7 @@ def quat_multiply(q1, q2, P=1): 083501. https://doi.org/10.1088/0965-0393/23/8/083501. """ - outer_shape = list(q1.shape[:-1]) + # outer_shape = list(q1.shape[:-1]) s1, v1 = q1[..., 0], q1[..., 1:] s2, v2 = q2[..., 0], q2[..., 1:] @@ -326,12 +326,12 @@ def compute_misorientation_matrix_damask(quat_comps, degrees=False, quiet=False) f"Finding misorientation for orientation {idx + 1}/{len(all_oris)}", flush=True, ) - ori_i = all_oris[idx : idx + 1] - other_oris = all_oris[idx + 1 :] + ori_i = all_oris[idx:idx + 1] + other_oris = all_oris[idx + 1:] if other_oris.size: disori_i = ori_i.disorientation(other_oris).as_axis_angle()[..., -1] - misori_matrix[idx, idx + 1 :] = disori_i - misori_matrix[idx + 1 :, idx] = disori_i + misori_matrix[idx, idx + 1:] = disori_i + misori_matrix[idx + 1:, idx] = disori_i if degrees: misori_matrix = np.rad2deg(misori_matrix) diff --git a/cipher_parse/utilities.py b/cipher_parse/utilities.py index 6340ae2..1df45fc 100644 --- a/cipher_parse/utilities.py +++ b/cipher_parse/utilities.py @@ -2,7 +2,6 @@ from importlib import resources import math from pathlib import Path -from functools import reduce import numpy as np from scipy.spatial import Voronoi, Delaunay @@ -460,8 +459,10 @@ def get_example_data_path_dream3D_3D(): def get_subset_indices(size, subset_size): - """Get a list of N indices that index as uniformly as possible a sequence of a given - size, with the constraint that the indices must include the initial and final elements. + """ + Get a list of N indices that index as uniformly as possible a sequence of a given + size, with the constraint that the indices must include the initial and final + elements. Parameters ----------- diff --git a/cipher_parse/voxel_map.py b/cipher_parse/voxel_map.py index b16518c..1399897 100644 --- a/cipher_parse/voxel_map.py +++ b/cipher_parse/voxel_map.py @@ -1,10 +1,14 @@ import numpy as np +from numpy.typing import NDArray import pyvista as pv from cipher_parse.utilities import get_array_edge_mask class VoxelMap: - def __init__(self, region_ID, size, is_periodic, region_data=None, quiet=False): + def __init__( + self, region_ID: NDArray, size: list[int] | tuple[int, ...], + is_periodic: bool, region_data: dict | None = None, quiet: bool = False + ): """ Parameters --------- @@ -27,7 +31,7 @@ def __init__(self, region_ID, size, is_periodic, region_data=None, quiet=False): if v.shape[0] != self.num_regions: raise ValueError( f"Region data must be the same length as the number of regions " - f"({self.num_regions}), but specified lenght for {k!r} was " + f"({self.num_regions}), but specified length for {k!r} was " f"{v.shape[0]}." ) self.region_data[k] = v @@ -35,52 +39,52 @@ def __init__(self, region_ID, size, is_periodic, region_data=None, quiet=False): self._coordinates = None # assigned by `get_coordinates` @property - def region_ID_flat(self): + def region_ID_flat(self) -> NDArray: return self.region_ID.reshape(-1) @property - def dimension(self): + def dimension(self) -> int: return self.region_ID.ndim @property - def grid_size(self): + def grid_size(self) -> NDArray: return np.array(self.region_ID.shape) @property - def shape(self): + def shape(self) -> tuple[int, ...]: return tuple(self.grid_size) @property - def spacing(self): + def spacing(self) -> NDArray: return self.size / self.grid_size @property - def spacing_3D(self): + def spacing_3D(self) -> NDArray: return self.size_3D / self.grid_size_3D @property - def num_voxels(self): - return np.product(self.grid_size) + def num_voxels(self) -> int: + return np.prod(self.grid_size) @property - def coordinates(self): + def coordinates(self) -> NDArray: if self._coordinates is None: self._coordinates = self._get_coordinates() return self._coordinates - def _get_coordinates(self): + def _get_coordinates(self) -> NDArray: mg_args = [np.arange(i) * j / i for i, j in zip(self.grid_size, self.size)] coords = np.concatenate([i[..., None] for i in np.meshgrid(*mg_args)], axis=-1) return coords - def generate_voxel_mask(self): + def generate_voxel_mask(self) -> NDArray: voxel_mask = np.zeros(self.shape, dtype=int) return voxel_mask.astype(bool) - def get_num_regions(self): + def get_num_regions(self) -> int: return np.unique(self.region_ID).size - def get_neighbour_region(self, dimension: int, direction: int): + def get_neighbour_region(self, dimension: int, direction: int) -> NDArray: """ Parameters ---------- @@ -105,49 +109,49 @@ def get_neighbour_region(self, dimension: int, direction: int): return region @property - def region_ID_above(self): + def region_ID_above(self) -> NDArray: return self.get_neighbour_region(self.dimension - 2, 1) @property - def region_ID_below(self): + def region_ID_below(self) -> NDArray: return self.get_neighbour_region(self.dimension - 2, -1) @property - def region_ID_left(self): + def region_ID_left(self) -> NDArray: return self.get_neighbour_region(self.dimension - 1, 1) @property - def region_ID_right(self): + def region_ID_right(self) -> NDArray: return self.get_neighbour_region(self.dimension - 1, -1) @property - def region_ID_in(self): + def region_ID_in(self) -> NDArray: if self.dimension != 3: raise AttributeError("No `region_ID_in` for 2D geometry.") else: return self.get_neighbour_region(0, 1) @property - def region_ID_out(self): + def region_ID_out(self) -> NDArray: if self.dimension != 3: raise AttributeError("No `region_ID_out` for 2D geometry.") else: return self.get_neighbour_region(0, -1) @property - def region_ID_diff_above(self): + def region_ID_diff_above(self) -> NDArray: return self.region_ID - self.region_ID_above != 0 @property - def region_ID_diff_below(self): + def region_ID_diff_below(self) -> NDArray: return self.region_ID - self.region_ID_below != 0 @property - def region_ID_diff_left(self): + def region_ID_diff_left(self) -> NDArray: return self.region_ID - self.region_ID_left != 0 @property - def region_ID_diff_right(self): + def region_ID_diff_right(self) -> NDArray: return self.region_ID - self.region_ID_right != 0 @property @@ -155,23 +159,23 @@ def region_ID_diff_in(self): return self.region_ID - self.region_ID_in != 0 @property - def region_ID_diff_out(self): + def region_ID_diff_out(self) -> NDArray: return self.region_ID - self.region_ID_out != 0 @property - def region_ID_diff_horz(self): + def region_ID_diff_horz(self) -> NDArray: return np.logical_or(self.region_ID_diff_left, self.region_ID_diff_right) @property - def region_ID_diff_vert(self): + def region_ID_diff_vert(self) -> NDArray: return np.logical_or(self.region_ID_diff_above, self.region_ID_diff_below) @property - def region_ID_diff_depth(self): + def region_ID_diff_depth(self) -> NDArray: return np.logical_or(self.region_ID_diff_in, self.region_ID_diff_out) @property - def region_ID_bulk(self): + def region_ID_bulk(self) -> NDArray: out = np.logical_and( np.logical_not(self.region_ID_diff_horz), np.logical_not(self.region_ID_diff_vert), @@ -181,7 +185,7 @@ def region_ID_bulk(self): return out - def get_region_boundary_voxels(self, r1: int, r2: int): + def get_region_boundary_voxels(self, r1: int, r2: int) -> NDArray: r1_vox = (self.region_ID == r1).astype(int) r2_vox = (self.region_ID == r2).astype(int) overlap = np.concatenate( @@ -210,7 +214,7 @@ def get_region_boundary_voxels(self, r1: int, r2: int): boundary_vox = np.sum(overlap, axis=0) > 0 return boundary_vox - def get_neighbour_voxels(self, quiet=False): + def get_neighbour_voxels(self, quiet: bool = False) -> NDArray: if not quiet: print("Finding neighbouring voxels...", end="") interface_voxels = np.copy(self.region_ID) @@ -219,13 +223,13 @@ def get_neighbour_voxels(self, quiet=False): print("done!") return interface_voxels - def get_interface_voxels(self): + def get_interface_voxels(self) -> NDArray: interface_voxels = np.copy(self.region_ID) interface_voxels[self.region_ID_bulk] = -1 interface_voxels[interface_voxels != -1] = 0 return interface_voxels - def get_neighbour_list(self, quiet=False): + def get_neighbour_list(self, quiet: bool = False) -> NDArray: """Get the pairs of regions that are neighbours""" if not quiet: print("Finding neighbour list...", end="") @@ -274,7 +278,7 @@ def get_neighbour_list(self, quiet=False): return neighbours - def get_interface_idx(self, interface_map, as_3D=False): + def get_interface_idx(self, interface_map: NDArray, as_3D: bool = False) -> NDArray: interface_idx_above_flat = interface_map[ self.region_ID_flat, self.region_ID_above.reshape(-1) ] @@ -337,20 +341,20 @@ def get_interface_idx(self, interface_map, as_3D=False): return interface_idx_all @property - def grid_size_3D(self): + def grid_size_3D(self) -> NDArray: if self.dimension == 2: return np.hstack([self.grid_size[::-1], 1]) else: return np.asarray(self.grid_size) @property - def size_3D(self): + def size_3D(self) -> NDArray: if self.dimension == 2: return np.hstack([self.size[::-1], self.size[0] / self.grid_size[0]]) else: return np.asarray(self.size) - def get_pyvista_grid(self, include_region_ID=False): + def get_pyvista_grid(self, include_region_ID: bool = False): """Experimental!""" grid = pv.ImageData()