diff --git a/gempy/core/color_generator.py b/gempy/core/color_generator.py index 61ca71e38..41964bb7a 100644 --- a/gempy/core/color_generator.py +++ b/gempy/core/color_generator.py @@ -3,6 +3,7 @@ from typing import Optional import numpy as np +from pydantic import Field @dataclass @@ -10,11 +11,8 @@ class ColorsGenerator: """ Object that handles the color management. """ - hex_colors: list[str] - _index: int = 0 - - def __init__(self): - self._gempy_default_colors = [ + + _gempy_default_colors = [ '#015482', '#9f0052', '#ffbe00', '#728f02', '#443988', '#ff3f20', '#5DA629', '#b271d0', '#72e54a', '#583bd1', '#d0e63d', '#b949e2', '#95ce4b', '#6d2b9f', '#60eb91', @@ -29,10 +27,16 @@ def __init__(self): '#945624', '#517c91', '#de8a68', '#3c4b64', '#9d8a4d', '#825f7e', '#2c3821', '#ddadaa', '#5e3524', '#a3a68e', '#a2706b', '#686d56' - ] # source: https://medialab.github.io/iwanthue/ - - self.regenerate_color_palette() + ] # source: https://medialab.github.io/iwanthue/ + hex_colors: list[str] = Field( + default=_gempy_default_colors, + exclude=True + ) + _index: int = 0 + + def __init__(self): + self.regenerate_color_palette() @staticmethod def _random_hexcolor() -> str: @@ -50,7 +54,7 @@ def regenerate_color_palette(self, seaborn_palettes: Optional[list[str]] = None) hex_colors = [] for palette in seaborn_palettes: # for each palette hex_colors += sns.color_palette(palette).as_hex() # get all colors in palette and add to list - + elif seaborn_palettes and not seaborn_installed: raise ImportError("Seaborn is not installed. Please install it to use color palettes.") else: @@ -58,11 +62,10 @@ def regenerate_color_palette(self, seaborn_palettes: Optional[list[str]] = None) self.hex_colors = hex_colors - def __iter__(self) -> 'ColorsGenerator': """Returns the object itself as an iterator.""" return self - + def __next__(self) -> str: """Generator that yields the next color.""" color = self.up_next() diff --git a/gempy/core/data/encoders/binary_encoder.py b/gempy/core/data/encoders/binary_encoder.py new file mode 100644 index 000000000..7c38fef9d --- /dev/null +++ b/gempy/core/data/encoders/binary_encoder.py @@ -0,0 +1,15 @@ +import numpy as np + +from ..surface_points import SurfacePointsTable +from ..orientations import OrientationsTable + + +def deserialize_input_data_tables(binary_array: bytes, name_id_map: dict, sp_binary_length_: int) -> tuple[OrientationsTable, SurfacePointsTable]: + sp_binary = binary_array[:sp_binary_length_] + ori_binary = binary_array[sp_binary_length_:] + # Reconstruct arrays + sp_data: np.ndarray = np.frombuffer(sp_binary, dtype=SurfacePointsTable.dt) + ori_data: np.ndarray = np.frombuffer(ori_binary, dtype=OrientationsTable.dt) + surface_points_table = SurfacePointsTable(data=sp_data, name_id_map=name_id_map) + orientations_table = OrientationsTable(data=ori_data, name_id_map=name_id_map) + return orientations_table, surface_points_table diff --git a/gempy/core/data/encoders/converters.py b/gempy/core/data/encoders/converters.py index 7ba135aad..76fd2f3d2 100644 --- a/gempy/core/data/encoders/converters.py +++ b/gempy/core/data/encoders/converters.py @@ -47,10 +47,9 @@ def instantiate_if_necessary(data: dict, key: str, type: type) -> None: loading_model_context = ContextVar('loading_model_context', default={}) @contextmanager -def loading_model_injection(surface_points_binary: np.ndarray, orientations_binary: np.ndarray): +def loading_model_from_binary(binary_body: bytes): token = loading_model_context.set({ - 'surface_points_binary': surface_points_binary, - 'orientations_binary' : orientations_binary + 'binary_body': binary_body, }) try: yield diff --git a/gempy/core/data/geo_model.py b/gempy/core/data/geo_model.py index 89f83eb3a..a62315862 100644 --- a/gempy/core/data/geo_model.py +++ b/gempy/core/data/geo_model.py @@ -302,23 +302,20 @@ def add_surface_points(self, X: Sequence[float], Y: Sequence[float], Z: Sequence @model_validator(mode='wrap') @classmethod def deserialize_properties(cls, data: Union["GeoModel", dict], constructor: ModelWrapValidatorHandler["GeoModel"]) -> "GeoModel": - try: - match data: - case GeoModel(): - return data - case dict(): - instance: GeoModel = constructor(data) - instantiate_if_necessary( - data=data, - key="_interpolation_options", - type=InterpolationOptions - ) - instance._interpolation_options = data.get("_interpolation_options") - return instance - case _: - raise ValidationError - except ValidationError: - raise + match data: + case GeoModel(): + return data + case dict(): + instance: GeoModel = constructor(data) + instantiate_if_necessary( + data=data, + key="_interpolation_options", + type=InterpolationOptions + ) + instance._interpolation_options = data.get("_interpolation_options") + return instance + case _: + raise ValidationError # endregion diff --git a/gempy/core/data/orientations.py b/gempy/core/data/orientations.py index 9aa6d22ee..a8222f924 100644 --- a/gempy/core/data/orientations.py +++ b/gempy/core/data/orientations.py @@ -1,13 +1,12 @@ from dataclasses import dataclass -from pydantic import field_validator, SkipValidation -from typing import Optional, Sequence, Union, Annotated +from typing import Optional, Sequence, Union import numpy as np +from gempy_engine.core.data.transforms import Transform +from pydantic import Field +from ...optional_dependencies import require_pandas from ._data_points_helpers import generate_ids_from_names -from .encoders.converters import numpy_array_short_validator -from gempy_engine.core.data.transforms import Transform -from gempy.optional_dependencies import require_pandas DEFAULT_ORI_NUGGET = 0.01 @@ -21,10 +20,15 @@ class OrientationsTable: A dataclass to represent a table of orientations in a geological model. """ - data: SkipValidation[np.ndarray] #: A structured NumPy array holding the X, Y, Z coordinates, gradients G_x, G_y, G_z, id, and nugget of each orientation. - name_id_map: Optional[dict[str, int]] = None #: A mapping between orientation names and ids. dt = np.dtype([('X', 'f8'), ('Y', 'f8'), ('Z', 'f8'), ('G_x', 'f8'), ('G_y', 'f8'), ('G_z', 'f8'), ('id', 'i4'), ('nugget', 'f8')]) #: The custom data type for the data array. + data: np.ndarray = Field( + default=np.zeros(0, dtype=dt), + exclude=True, + description="A structured NumPy array holding the X, Y, Z coordinates, gradients G_x, G_y, G_z, id, and nugget of each orientation.", + ) #: A structured NumPy array holding the X, Y, Z coordinates, id, and nugget of each surface point. + name_id_map: Optional[dict[str, int]] = None #: A mapping between orientation names and ids. + _model_transform: Optional[Transform] = None @@ -58,11 +62,6 @@ def from_arrays(cls, x: np.ndarray, y: np.ndarray, z: np.ndarray, data, name_id_map = cls._data_from_arrays(x, y, z, G_x, G_y, G_z, names, nugget, name_id_map) return cls(data, name_id_map) - @field_validator('data', mode='after') - @classmethod - def parse_short_array(cls, _: list[list]) -> str: - # Now just build a structured array - return np.zeros(0, dtype=OrientationsTable.dt) @classmethod def _data_from_arrays(cls, x, y, z, G_x, G_y, G_z, names, nugget, name_id_map=None) -> tuple[np.ndarray, dict[str, int]]: diff --git a/gempy/core/data/structural_frame.py b/gempy/core/data/structural_frame.py index f43cda65d..98d01fd25 100644 --- a/gempy/core/data/structural_frame.py +++ b/gempy/core/data/structural_frame.py @@ -3,12 +3,15 @@ import numpy as np import warnings from dataclasses import dataclass -from pydantic import model_validator, computed_field -from typing import Generator +from pydantic import model_validator, computed_field, ValidationError +from pydantic.functional_validators import ModelWrapValidatorHandler +from typing import Generator, Union from gempy_engine.core.data.input_data_descriptor import InputDataDescriptor from gempy_engine.core.data.kernel_classes.faults import FaultsData from gempy_engine.core.data.stack_relation_type import StackRelationType + +from .encoders.binary_encoder import deserialize_input_data_tables from .encoders.converters import loading_model_context from .orientations import OrientationsTable from .structural_element import StructuralElement @@ -29,94 +32,15 @@ class StructuralFrame: """ structural_groups: list[StructuralGroup] - color_generator: ColorsGenerator # ? Should I create some sort of structural options class? For example, the masking descriptor and faults relations pointer is_dirty: bool = True - @model_validator(mode="after") - def deserialize_surface_points(values: "StructuralFrame"): - # Access the context variable to get injected data - context = loading_model_context.get() - - if 'surface_points_binary' not in context: - return values - - # Check if we have a binary payload to digest - binary_array = context['surface_points_binary'] - if not isinstance(binary_array, np.ndarray): - return values - if binary_array.shape[0] < 1: - return values - - values.surface_points = SurfacePointsTable( - data=binary_array, - name_id_map=values.surface_points_copy.name_id_map - ) - - return values - - @model_validator(mode="after") - def deserialize_orientations(values: "StructuralFrame"): - # Access the context variable to get injected data - context = loading_model_context.get() - if 'orientations_binary' not in context: - return values - - # Check if we have a binary payload to digest - binary_array = context['orientations_binary'] - if not isinstance(binary_array, np.ndarray): - return values - - values.orientations = OrientationsTable( - data=binary_array, - name_id_map=values.orientations_copy.name_id_map - ) - - return values - - - @computed_field - @property - def serialize_sp(self) -> int: - return int(hashlib.md5(self.surface_points_copy.data.tobytes()).hexdigest()[:8], 16) - - @computed_field - @property - def serialize_orientations(self) -> int: - return int(hashlib.md5(self.orientations_copy.data.tobytes()).hexdigest()[:8], 16) + # region Constructor def __init__(self, structural_groups: list[StructuralGroup], color_gen: ColorsGenerator): self.structural_groups = structural_groups # ? This maybe could be optional self.color_generator = color_gen - def get_element_by_name(self, element_name: str) -> StructuralElement: - elements: Generator = (group.get_element_by_name(element_name) for group in self.structural_groups) - valid_elements: Generator = (element for element in elements if element is not None) - element = next(valid_elements, None) - if element is None: - raise ValueError(f"Element with name {element_name} not found in the structural frame.") - return element - - def get_group_by_name(self, group_name: str) -> StructuralGroup: - groups: Generator = (group for group in self.structural_groups if group.name == group_name) - group = next(groups, None) - if group is None: - raise ValueError(f"Group with name {group_name} not found in the structural frame.") - return group - - def get_group_by_element(self, element: StructuralElement) -> StructuralGroup: - groups: Generator = (group for group in self.structural_groups if element in group.elements) - group = next(groups, None) - if group is None: - raise ValueError(f"Element {element.name} not found in any group in the structural frame.") - return group - - def append_group(self, group: StructuralGroup): - self.structural_groups.append(group) - - def insert_group(self, index: int, group: StructuralGroup): - self.structural_groups.insert(index, group) - @classmethod def from_data_tables(cls, surface_points: SurfacePointsTable, orientations: OrientationsTable): surface_points_groups: list[SurfacePointsTable] = surface_points.get_surface_points_by_id_groups() @@ -190,6 +114,37 @@ def initialize_default_structure(cls) -> 'StructuralFrame': return structural_frame + # endregion + + # region Methods + def get_element_by_name(self, element_name: str) -> StructuralElement: + elements: Generator = (group.get_element_by_name(element_name) for group in self.structural_groups) + valid_elements: Generator = (element for element in elements if element is not None) + element = next(valid_elements, None) + if element is None: + raise ValueError(f"Element with name {element_name} not found in the structural frame.") + return element + + def get_group_by_name(self, group_name: str) -> StructuralGroup: + groups: Generator = (group for group in self.structural_groups if group.name == group_name) + group = next(groups, None) + if group is None: + raise ValueError(f"Group with name {group_name} not found in the structural frame.") + return group + + def get_group_by_element(self, element: StructuralElement) -> StructuralGroup: + groups: Generator = (group for group in self.structural_groups if element in group.elements) + group = next(groups, None) + if group is None: + raise ValueError(f"Element {element.name} not found in any group in the structural frame.") + return group + + def append_group(self, group: StructuralGroup): + self.structural_groups.append(group) + + def insert_group(self, index: int, group: StructuralGroup): + self.structural_groups.insert(index, group) + def __repr__(self): structural_groups_repr = ',\n'.join([repr(g) for g in self.structural_groups]) fault_relations_str = np.array2string(self.fault_relations, precision=2, separator=', ', suppress_small=True) if self.fault_relations is not None else 'None' @@ -230,6 +185,9 @@ def _repr_html_(self): """ return html + # endregion + + # region Properties @property def structural_elements(self) -> list[StructuralElement]: """Returns a list of all structural elements across the structural groups.""" @@ -432,6 +390,10 @@ def orientations(self, modified_orientations: OrientationsTable) -> None: """Distributes the modified orientations back to the structural elements.""" for element in self.structural_elements: element.orientations.data = modified_orientations.get_orientations_by_id(element.id).data + + @property + def input_tables_binary(self): + return self.surface_points_copy.data.tobytes() + self.orientations_copy.data.tobytes() @property def element_id_name_map(self) -> dict[int, str]: @@ -500,6 +462,45 @@ def surfaces_df(self) -> 'pd.DataFrame': # endregion + # endregion + # region Pydantic + + @model_validator(mode="wrap") + @classmethod + def deserialize_binary(cls, data: Union["StructuralFrame", dict], constructor: ModelWrapValidatorHandler["StructuralFrame"]) -> "StructuralFrame": + match data: + case StructuralFrame(): + return data + case dict(): + instance: StructuralFrame = constructor(data) + metadata = data.get('binary_meta_data', {}) + context = loading_model_context.get() + + if 'binary_body' not in context: + return instance + + instance.orientations, instance.surface_points = deserialize_input_data_tables( + binary_array=context['binary_body'], + name_id_map=instance.surface_points_copy.name_id_map, + sp_binary_length_=metadata["sp_binary_length"] + ) + + return instance + case _: + raise ValidationError(f"Invalid data type for StructuralFrame: {type(data)}") + + # Access the context variable to get injected data + + + @computed_field + def binary_meta_data(self) -> dict: + return { + 'sp_binary_length': len(self.surface_points_copy.data.tobytes()), + # 'ori_binary_length': len(self.orientations_copy.data.tobytes()) * (miguel May 2025) This is not necessary at the moment + } + + # endregion + def _validate_faults_relations(self): """Check that if there are any StackRelationType.FAULT in the structural groups the fault relation matrix is given and shape is the right one, i.e. a square matrix of size equals to len(groups)""" diff --git a/gempy/core/data/surface_points.py b/gempy/core/data/surface_points.py index 899d145ac..bf4ead77f 100644 --- a/gempy/core/data/surface_points.py +++ b/gempy/core/data/surface_points.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from pydantic import field_validator, SkipValidation -from typing import Optional, Union, Sequence, Annotated +from typing import Optional, Union, Sequence + import numpy as np +from gempy_engine.core.data.transforms import Transform +from pydantic import Field from ._data_points_helpers import generate_ids_from_names -from .encoders.converters import numpy_array_short_validator -from gempy_engine.core.data.transforms import Transform -from gempy.optional_dependencies import require_pandas +from ...optional_dependencies import require_pandas DEFAULT_SP_NUGGET = 0.00002 @@ -22,10 +22,14 @@ class SurfacePointsTable: A dataclass to represent a table of surface points in a geological model. """ - data: SkipValidation[np.ndarray] #: A structured NumPy array holding the X, Y, Z coordinates, id, and nugget of each surface point. - name_id_map: Optional[dict[str, int]] = None #: A mapping between surface point names and ids. - dt = np.dtype([('X', 'f8'), ('Y', 'f8'), ('Z', 'f8'), ('id', 'i4'), ('nugget', 'f8')]) #: The custom data type for the data array. + + data: np.ndarray = Field( + default=np.zeros(0, dtype=dt), + exclude=True, + description="A structured NumPy array holding the X, Y, Z coordinates, id, and nugget of each surface point." + ) #: A structured NumPy array holding the X, Y, Z coordinates, id, and nugget of each surface point. + name_id_map: Optional[dict[str, int]] = None #: A mapping between surface point names and ids. _model_transform: Optional[Transform] = None def __post_init__(self): @@ -77,12 +81,6 @@ def from_arrays(cls, x: np.ndarray, y: np.ndarray, z: np.ndarray, return cls(data, name_id_map) - @field_validator('data', mode='after') - @classmethod - def parse_short_array(cls, _: list[list]) -> str: - # Now just build a structured array - return np.zeros(0, dtype=SurfacePointsTable.dt) - @classmethod def _data_from_arrays(cls, x: np.ndarray, y: np.ndarray, z: np.ndarray, names: Union[Sequence | str], nugget: Optional[np.ndarray] = None, diff --git a/gempy/modules/serialization/__init__.py b/gempy/modules/serialization/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gempy/modules/serialization/save_load.py b/gempy/modules/serialization/save_load.py new file mode 100644 index 000000000..1008b9fad --- /dev/null +++ b/gempy/modules/serialization/save_load.py @@ -0,0 +1,128 @@ +from ...core.data import GeoModel +from ...core.data.encoders.converters import loading_model_from_binary +from ...optional_dependencies import require_zlib +import pathlib +import os + + +def save_model(model: GeoModel, path: str, validate_serialization: bool = True): + """ + Save a GeoModel to a file with proper extension validation. + + Parameters: + ----------- + model : GeoModel + The geological model to save + path : str + The file path where to save the model + + Raises: + ------- + ValueError + If the file has an extension other than .gempy + """ + # Define the valid extension for gempy models + VALID_EXTENSION = ".gempy" + + # Check if path has an extension + path_obj = pathlib.Path(path) + if path_obj.suffix: + # If extension exists but is not valid, raise error + if path_obj.suffix.lower() != VALID_EXTENSION: + raise ValueError(f"Invalid file extension: {path_obj.suffix}. Expected: {VALID_EXTENSION}") + else: + # If no extension, add the valid extension + path = str(path_obj) + VALID_EXTENSION + + model_json = model.model_dump_json(by_alias=True, indent=4) + + # Compress the binary data + zlib = require_zlib() + compressed_binary = zlib.compress(model.structural_frame.input_tables_binary) + + binary_file = _to_binary(model_json, compressed_binary) + + if validate_serialization: + model_deserialized = _deserialize_binary_file(binary_file) + _validate_serialization(model, model_deserialized) + + # Create directory if it doesn't exist + directory = os.path.dirname(path) + if directory and not os.path.exists(directory): + os.makedirs(directory) + + with open(path, 'wb') as f: + f.write(binary_file) + + return path # Return the actual path used (helpful if extension was added) + + +def load_model(path: str) -> GeoModel: + """ + Load a GeoModel from a file with extension validation. + + Parameters: + ----------- + path : str + Path to the gempy model file + + Returns: + -------- + GeoModel + The loaded geological model + + Raises: + ------- + ValueError + If the file doesn't have the proper .gempy extension + FileNotFoundError + If the file doesn't exist + """ + VALID_EXTENSION = ".gempy" + + # Check if path has the valid extension + path_obj = pathlib.Path(path) + if not path_obj.suffix or path_obj.suffix.lower() != VALID_EXTENSION: + raise ValueError(f"Invalid file extension: {path_obj.suffix}. Expected: {VALID_EXTENSION}") + + # Check if file exists + if not os.path.exists(path): + raise FileNotFoundError(f"File not found: {path}") + + with open(path, 'rb') as f: + binary_file = f.read() + + return _deserialize_binary_file(binary_file) + + +def _deserialize_binary_file(binary_file): + # Get header length from first 4 bytes + header_length = int.from_bytes(binary_file[:4], byteorder='little') + # Split header and body + header_json = binary_file[4:4 + header_length].decode('utf-8') + binary_body = binary_file[4 + header_length:] + zlib = require_zlib() + decompressed_binary = zlib.decompress(binary_body) + with loading_model_from_binary( + binary_body=decompressed_binary, + ): + model = GeoModel.model_validate_json(header_json) + return model + + +def _to_binary(header_json, body_) -> bytes: + header_json_bytes = header_json.encode('utf-8') + header_json_length = len(header_json_bytes) + header_json_length_bytes = header_json_length.to_bytes(4, byteorder='little') + file = header_json_length_bytes + header_json_bytes + body_ + return file + + +def _validate_serialization(original_model, model_deserialized): + a = hash(original_model.structural_frame.surface_points_copy.data.tobytes()) + b = hash(model_deserialized.structural_frame.surface_points_copy.data.tobytes()) + o_a = hash(original_model.structural_frame.orientations_copy.data.tobytes()) + o_b = hash(model_deserialized.structural_frame.orientations_copy.data.tobytes()) + assert a == b, "Hashes for surface points are not equal" + assert o_a == o_b, "Hashes for orientations are not equal" + assert model_deserialized.__str__() == original_model.__str__() diff --git a/gempy/optional_dependencies.py b/gempy/optional_dependencies.py index 6f07ffb14..575e81036 100644 --- a/gempy/optional_dependencies.py +++ b/gempy/optional_dependencies.py @@ -58,4 +58,11 @@ def require_subsurface(): import subsurface except ImportError: raise ImportError("The subsurface package is required to run this function.") - return subsurface \ No newline at end of file + return subsurface + +def require_zlib(): + try: + import zlib + except ImportError: + raise ImportError("The zlib package is required to run this function.") + return zlib \ No newline at end of file diff --git a/test/test_modules/test_serialize_model.py b/test/test_modules/test_serialize_model.py index e7b78e0f0..8c3c281ca 100644 --- a/test/test_modules/test_serialize_model.py +++ b/test/test_modules/test_serialize_model.py @@ -2,16 +2,18 @@ import os import pprint +from gempy_engine.core.data import InterpolationOptions + import gempy as gp -from gempy.core.data.encoders.converters import loading_model_injection +from gempy.core.data.encoders.converters import loading_model_from_binary from gempy.core.data.enumerators import ExampleModel -from gempy_engine.core.data import InterpolationOptions +from gempy.modules.serialization.save_load import save_model, load_model from test.verify_helper import verify_json def test_generate_horizontal_stratigraphic_model(): model: gp.data.GeoModel = gp.generate_example_model(ExampleModel.HORIZONTAL_STRAT, compute_model=False) - model_json = model.model_dump_json(by_alias=True, indent=4, exclude={"*data"}) + model_json = model.model_dump_json(by_alias=True, indent=4) # Write the JSON to disk if False: @@ -19,21 +21,12 @@ def test_generate_horizontal_stratigraphic_model(): with open(file_path, "w") as f: f.write(model_json) - with loading_model_injection( - surface_points_binary=model.structural_frame.surface_points_copy.data, # TODO: Here we need to pass the binary array - orientations_binary=model.structural_frame.orientations_copy.data + with loading_model_from_binary( + binary_body=model.structural_frame.input_tables_binary ): model_deserialized = gp.data.GeoModel.model_validate_json(model_json) - a = hash(model.structural_frame.structural_elements[1].surface_points.data.tobytes()) - b = hash(model_deserialized.structural_frame.structural_elements[1].surface_points.data.tobytes()) - - o_a = hash(model.structural_frame.structural_elements[1].orientations.data.tobytes()) - o_b = hash(model_deserialized.structural_frame.structural_elements[1].orientations.data.tobytes()) - - assert a == b, "Hashes for surface points are not equal" - assert o_a == o_b, "Hashes for orientations are not equal" - assert model_deserialized.__str__() == model.__str__() + _validate_serialization(model, model_deserialized) # # Validate json against schema if True: @@ -42,7 +35,32 @@ def test_generate_horizontal_stratigraphic_model(): verify_model = json.loads(model_json) verify_model["meta"]["creation_date"] = "" verify_json(json.dumps(verify_model, indent=4), name="verify/Horizontal Stratigraphic Model serialization") - + + +def _validate_serialization(original_model, model_deserialized): + a = hash(original_model.structural_frame.surface_points_copy.data.tobytes()) + b = hash(model_deserialized.structural_frame.surface_points_copy.data.tobytes()) + o_a = hash(original_model.structural_frame.orientations_copy.data.tobytes()) + o_b = hash(model_deserialized.structural_frame.orientations_copy.data.tobytes()) + assert a == b, "Hashes for surface points are not equal" + assert o_a == o_b, "Hashes for orientations are not equal" + assert model_deserialized.__str__() == original_model.__str__() + + +def test_save_model_to_disk(): + model = gp.generate_example_model(ExampleModel.COMBINATION, compute_model=False) + save_model(model, "temp/test_save_model_to_disk.gempy") + + # Load the model from disk + loaded_model = load_model("temp/test_save_model_to_disk.gempy") + _validate_serialization(model, loaded_model) + + gp.compute_model(loaded_model) + if True: + import gempy_viewer as gpv + gpv.plot_3d(loaded_model, image=True) + + def test_interpolation_options(): diff --git a/test/test_modules/test_serialize_model.test_generate_horizontal_stratigraphic_model.verify/Horizontal Stratigraphic Model serialization.approved.txt b/test/test_modules/test_serialize_model.test_generate_horizontal_stratigraphic_model.verify/Horizontal Stratigraphic Model serialization.approved.txt index a277aa565..6e0892088 100644 --- a/test/test_modules/test_serialize_model.test_generate_horizontal_stratigraphic_model.verify/Horizontal Stratigraphic Model serialization.approved.txt +++ b/test/test_modules/test_serialize_model.test_generate_horizontal_stratigraphic_model.verify/Horizontal Stratigraphic Model serialization.approved.txt @@ -15,50 +15,6 @@ "is_active": true, "_color": "#9f0052", "surface_points": { - "data": [ - [ - 100.0, - 200.0, - 600.0, - 117776925, - 2e-05 - ], - [ - 500.0, - 200.0, - 600.0, - 117776925, - 2e-05 - ], - [ - 900.0, - 200.0, - 600.0, - 117776925, - 2e-05 - ], - [ - 100.0, - 800.0, - 600.0, - 117776925, - 2e-05 - ], - [ - 500.0, - 800.0, - 600.0, - 117776925, - 2e-05 - ], - [ - 900.0, - 800.0, - 600.0, - 117776925, - 2e-05 - ] - ], "name_id_map": { "rock1": 67239155, "rock2": 117776925 @@ -66,18 +22,6 @@ "_model_transform": null }, "orientations": { - "data": [ - [ - 500.0, - 500.0, - 600.0, - 0.0, - 0.0, - 1.0, - 117776925, - 0.01 - ] - ], "name_id_map": { "rock1": 67239155, "rock2": 117776925 @@ -94,50 +38,6 @@ "is_active": true, "_color": "#015482", "surface_points": { - "data": [ - [ - 100.0, - 200.0, - 400.0, - 67239155, - 2e-05 - ], - [ - 500.0, - 200.0, - 400.0, - 67239155, - 2e-05 - ], - [ - 900.0, - 200.0, - 400.0, - 67239155, - 2e-05 - ], - [ - 100.0, - 800.0, - 400.0, - 67239155, - 2e-05 - ], - [ - 500.0, - 800.0, - 400.0, - 67239155, - 2e-05 - ], - [ - 900.0, - 800.0, - 400.0, - 67239155, - 2e-05 - ] - ], "name_id_map": { "rock1": 67239155, "rock2": 117776925 @@ -145,18 +45,6 @@ "_model_transform": null }, "orientations": { - "data": [ - [ - 500.0, - 500.0, - 400.0, - 0.0, - 0.0, - 1.0, - 67239155, - 0.01 - ] - ], "name_id_map": { "rock1": 67239155, "rock2": 117776925 @@ -175,82 +63,11 @@ "solution": null } ], - "color_generator": { - "hex_colors": [ - "#015482", - "#9f0052", - "#ffbe00", - "#728f02", - "#443988", - "#ff3f20", - "#5DA629", - "#b271d0", - "#72e54a", - "#583bd1", - "#d0e63d", - "#b949e2", - "#95ce4b", - "#6d2b9f", - "#60eb91", - "#d746be", - "#52a22e", - "#5e63d8", - "#e5c339", - "#371970", - "#d3dc76", - "#4d478e", - "#43b665", - "#d14897", - "#59e5b8", - "#e5421d", - "#62dedb", - "#df344e", - "#9ce4a9", - "#d94077", - "#99c573", - "#842f74", - "#578131", - "#708de7", - "#df872f", - "#5a73b1", - "#ab912b", - "#321f4d", - "#e4bd7c", - "#142932", - "#cd4f30", - "#69aedd", - "#892a23", - "#aad6de", - "#5c1a34", - "#cfddb4", - "#381d29", - "#5da37c", - "#d8676e", - "#52a2a3", - "#9b405c", - "#346542", - "#de91c9", - "#555719", - "#bbaed6", - "#945624", - "#517c91", - "#de8a68", - "#3c4b64", - "#9d8a4d", - "#825f7e", - "#2c3821", - "#ddadaa", - "#5e3524", - "#a3a68e", - "#a2706b", - "#686d56" - ], - "_index": 2 - }, "is_dirty": true, "basement_color": "#ffbe00", - "serialize_sp": 3507338795, - "serialize_orientations": 553806131 + "binary_meta_data": { + "sp_binary_length": 432 + } }, "grid": { "_octree_grid": null,