• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zincware / MDSuite / 3999396905

pending completion
3999396905

push

github-actions

GitHub
[merge before other PRs] ruff updates (#580)

960 of 1311 branches covered (73.23%)

Branch coverage included in aggregate %.

15 of 15 new or added lines in 11 files covered. (100.0%)

4034 of 4930 relevant lines covered (81.83%)

3.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.99
/mdsuite/experiment/experiment.py
1
"""
2
MDSuite: A Zincwarecode package.
3

4
License
5
-------
6
This program and the accompanying materials are made available under the terms
7
of the Eclipse Public License v2.0 which accompanies this distribution, and is
8
available at https://www.eclipse.org/legal/epl-v20.html
9

10
SPDX-License-Identifier: EPL-2.0
11

12
Copyright Contributors to the Zincwarecode Project.
13

14
Contact Information
15
-------------------
16
email: zincwarecode@gmail.com
17
github: https://github.com/zincware
18
web: https://zincwarecode.com/
19

20
Citation
21
--------
22
If you use this module please cite us with:
23

24
Summary
25
-------
26
"""
27
import copy
4✔
28
import importlib.resources
4✔
29
import json
4✔
30
import logging
4✔
31
import pathlib
4✔
32
import typing
4✔
33
from pathlib import Path
4✔
34
from typing import List, Union
4✔
35

36
import numpy as np
4✔
37
import pubchempy as pcp
4✔
38

39
import mdsuite.file_io.extxyz_files
4✔
40
import mdsuite.file_io.file_read
4✔
41
import mdsuite.file_io.lammps_trajectory_files
4✔
42
import mdsuite.utils.meta_functions
4✔
43
from mdsuite.database.experiment_database import ExperimentDatabase
4✔
44
from mdsuite.database.simulation_database import (
4✔
45
    Database,
46
    SpeciesInfo,
47
    TrajectoryMetadata,
48
)
49
from mdsuite.experiment.run import RunComputation
4✔
50
from mdsuite.time_series import time_series_dict
4✔
51
from mdsuite.transformations import Transformations
4✔
52
from mdsuite.utils import config
4✔
53
from mdsuite.utils.exceptions import ElementMassAssignedZero
4✔
54
from mdsuite.utils.meta_functions import join_path
4✔
55
from mdsuite.utils.units import Units, units_dict
4✔
56

57
from .run_module import RunModule
4✔
58

59
log = logging.getLogger(__name__)
4✔
60

61

62
def _get_processor(simulation_data):
4✔
63
    """Read in one file."""
64
    if isinstance(simulation_data, str) or isinstance(simulation_data, pathlib.Path):
4✔
65
        suffix = pathlib.Path(simulation_data).suffix
4✔
66
        if suffix == ".lammpstraj":
4✔
67
            processor = mdsuite.file_io.lammps_trajectory_files.LAMMPSTrajectoryFile(
4✔
68
                simulation_data
69
            )
70
        elif suffix == ".extxyz":
4!
71
            processor = mdsuite.file_io.extxyz_files.EXTXYZFile(simulation_data)
4✔
72
        else:
73
            raise ValueError(
×
74
                f"datafile ending '{suffix}' not recognized. If there is a reader for"
75
                " your file type, you will find it in mdsuite.file_io."
76
            )
77
    elif isinstance(simulation_data, mdsuite.file_io.file_read.FileProcessor):
4!
78
        processor = simulation_data
4✔
79
    else:
80
        raise ValueError(
×
81
            "simulation_data must be either str, pathlib.Path or instance of"
82
            f" mdsuite.file_io.file_read.FileProcessor. Got '{type(simulation_data)}'"
83
            " instead"
84
        )
85

86
    return processor
4✔
87

88

89
class Experiment(ExperimentDatabase):
4✔
90
    """
91
    The central experiment class fundamental to all analysis.
92

93
    .. code-block:: python
94

95
        project = mdsuite.Project()
96
        project.add_experiment(
97
            name="NaCl",
98
            timestep=0.002,
99
            temperature=1400.0,
100
            units="metal",
101
            simulation_data="NaCl_gk_i_q.lammpstraj"
102
            )
103
        project.experiments.NaCl.run.RadialDistributionFunction(
104
            number_of_configurations=500
105
        )
106

107

108
    Attributes
109
    ----------
110
    name : str
111
            The name of the analysis being performed e.g. NaCl_1400K
112
    storage_path : str
113
            Path to where the tensor_values should be stored (best to have  drive
114
            capable of storing large files)
115
    temperature : float
116
            The temperature of the simulation that should be used in some analysis.
117
            Necessary as it cannot be easily read in from the simulation tensor_values.
118
    time_step : float
119
            Time step of the simulation e.g 0.002. Necessary as it cannot be easily
120
            read in from the trajectory.
121
    volume : float
122
            Volume of the simulation box
123
    species : dict
124
            A dictionary of the species in the experiment and their properties.
125
            Their properties includes index location in the trajectory file, mass of
126
            the species as taken from the PubChem database_path, and the charge taken
127
            from the same database_path. When using these properties, it is best that
128
            users confirm this information, with exception to the indices as they are
129
            read from the file and will be correct.
130
    number_of_atoms : int
131
            The total number of atoms in the simulation
132
    """
133

134
    def __init__(
4✔
135
        self,
136
        project,
137
        name,
138
        time_step=None,
139
        temperature=None,
140
        units: Union[str, Units] = None,
141
        cluster_mode=False,
142
    ):
143
        """
144
        Initialise the experiment class.
145

146
        Attributes
147
        ----------
148
        name : str
149
                The name of the analysis being performed e.g. NaCl_1400K
150
        temperature : float
151
                The temperature of the simulation that should be used in some analysis.
152
        time_step : float
153
                Time step of the simulation e.g 0.002. Necessary as it cannot be easily
154
                read in from the trajectory.
155
        units: Union[str, dict], default = "real"
156
            The units to be used in the experiment to convert to SI
157
        cluster_mode : bool
158
                If true, several parameters involved in plotting and parallelization
159
                will be adjusted so as to allow for optimal performance on a large
160
                computing cluster.
161
        """
162
        if not name[0].isalpha():
4✔
163
            raise ValueError(
4✔
164
                f"Experiment name must start with a letter! Found '{name[0]}' instead."
165
            )
166

167
        # Taken upon instantiation
168
        super().__init__(project=project, name=name)
4✔
169
        self.name = name
4✔
170
        self.storage_path = Path(project.storage_path, project.name).as_posix()
4✔
171
        self.cluster_mode = cluster_mode
4✔
172

173
        # ExperimentDatabase stored properties:
174
        # ------- #
175
        # set default values
176
        if self.number_of_configurations is None:
4✔
177
            self.number_of_configurations = 0
4✔
178
        # update database (None values are ignored)
179
        self.temperature = temperature
4✔
180
        self.time_step = time_step
4✔
181

182
        # Available properties that aren't set on default
183
        self.number_of_atoms = None
4✔
184
        # ------- #
185

186
        # Added from trajectory file
187

188
        if self.units is None:
4✔
189
            if units is None:
4✔
190
                units = mdsuite.units.REAL
4✔
191
            self.units = self.units_to_si(units)  # Units used during the simulation.
4✔
192

193
        self.box_array = None  # Box vectors.
4✔
194
        self.dimensions = None  # Dimensionality of the experiment.
4✔
195

196
        self.sample_rate = (
4✔
197
            None  # Rate at which configurations are dumped in the trajectory.
198
        )
199
        self.properties = None  # Properties measured in the simulation.
4✔
200
        self.property_groups = None  # Names of the properties measured in the simulation
4✔
201

202
        # Internal File paths
203
        self.path: Path
4✔
204
        self.database_path: Path
4✔
205
        self.figures_path: Path
4✔
206
        self._create_internal_file_paths()  # fill the path attributes
4✔
207

208
        # Check if the experiment exists and load if it does.
209
        self._load_or_build()
4✔
210

211
        self.analyse_time_series = RunModule(self, time_series_dict)
4✔
212

213
    @property
4✔
214
    def run(self) -> RunComputation:
4✔
215
        """Method to access the available calculators.
216

217
        Returns
218
        -------
219
        RunComputation:
220
            class that has all available calculators as properties
221
        """
222
        return RunComputation(experiment=self)
4✔
223

224
    def __repr__(self):
4✔
225
        """
226
        Representation of the class.
227

228
        In our case, the representation of the class is the name of the experiment.
229
        """
230
        return f"exp_{self.name}"
4✔
231

232
    def _create_internal_file_paths(self):
4✔
233
        """Create or update internal file paths.
234

235
        Attributes
236
        ----------
237
        path: Path
238
            The default path for the experiment files
239
        database_path: Path
240
            Path to the database, by default equal to self.path
241
        figures_path: Path
242
            Path to the figures directory
243

244
        """
245
        self.path = Path(self.storage_path, self.name)  # path to the experiment files
4✔
246
        self.database_path = self.path  # path to the databases
4✔
247
        self.figures_path = self.path / "figures"  # path to the figures directory
4✔
248

249
    def _build_model(self):
4✔
250
        """
251
        Build the 'experiment' for the analysis.
252

253
        A method to build the database_path in the hdf5 format. Within this method,
254
        several other are called to develop the database_path skeleton,
255
        get configurations, and process and store the configurations. The method is
256
        accompanied by a loading bar which should be customized to make it more
257
        interesting.
258
        """
259
        # Create new analysis directory and change into it
260
        try:
4✔
261
            self.path.mkdir()
4✔
262
            self.figures_path.mkdir()
4✔
263
            self.database_path.mkdir()
4✔
264
        except FileExistsError:  # throw exception if the file exits
4✔
265
            return
4✔
266

267
        # self.save_class()  # save the class state.
268
        log.info(f"** An experiment has been added titled {self.name} **")
×
269

270
    def cls_transformation_run(self, transformation: Transformations, *args, **kwargs):
4✔
271
        """Run the transformation.
272

273
        The Transformation class is updated with this experiment and afterwards
274
        performs the transformation.
275
        Preliminary work in accordance to https://github.com/zincware/MDSuite/issues/404
276

277
        Parameters
278
        ----------
279
        transformation: Transformations
280
        """
281
        transformation.experiment = self
4✔
282
        transformation.run_transformation(*args, **kwargs)
4✔
283

284
    @staticmethod
4✔
285
    def units_to_si(units_system) -> Units:
4✔
286
        """
287
        Returns a dictionary with equivalences from the unit experiment given by a
288
        string to SI. Along with some constants in the unit experiment provided
289
        (boltzmann, or other conversions). Instead, the user may provide a dictionary.
290
        In that case, the dictionary will be used as the unit experiment.
291

292

293
        Parameters
294
        ----------
295
        units_system (str) -- current unit experiment
296
        dimension (str) -- dimension you would like to change
297

298
        Returns
299
        -------
300
        units: Units
301
            dataclass that contains the conversion factors to SI
302
        """
303
        if isinstance(units_system, Units):
4✔
304
            return units_system
4✔
305
        elif isinstance(units_system, str):
4!
306
            try:
4✔
307
                units = units_dict[units_system]
4✔
308
            except KeyError:
×
309
                raise KeyError(
×
310
                    f"The unit '{units_system}' is not implemented."
311
                    f" The available units are: {list(units_dict)}"
312
                )
313
        else:
314
            raise ValueError(
×
315
                "units has to be of type Units or str,"
316
                f" found {type(units_system)} instead"
317
            )
318
        return units
4✔
319

320
    def _load_or_build(self) -> bool:
4✔
321
        """
322
        Check if the experiment already exists and decide whether to load it or build a
323
        new one.
324
        """
325
        # Check if the experiment exists and load if it does.
326
        if Path(self.path).exists():
4✔
327
            log.debug(
4✔
328
                f"This experiment ({self.name}) already exists! I'll load it up now."
329
            )
330
            return True
4✔
331
        else:
332
            log.info(f"Creating a new experiment ({self.name})!")
4✔
333
            self._build_model()
4✔
334
            return False
4✔
335

336
    def run_visualization(
4✔
337
        self,
338
        species: list = None,
339
        molecules: bool = False,
340
        unwrapped: bool = False,
341
    ):
342
        """
343
        Perform a trajectory visualization.
344

345
        Parameters
346
        ----------
347
        species : list
348
                A list of species of molecules to study.
349
        molecules : bool
350
                If true, molecule groups will be used.
351
        unwrapped : bool
352
                If true, unwrapped coordinates will be visualized.
353

354
        Returns
355
        -------
356
        Displays a visualization app.
357
        """
358
        import_error_msg = (
×
359
            "It looks like you don't have the necessary plugin for "
360
            "the visualizer extension. Please install znvis with"
361
            " pip install znvis in order to use the MDSuite visualizer."
362
        )
363
        try:
×
364
            from mdsuite.visualizer.znvis_visualizer import SimulationVisualizer
×
365
        except ImportError:
×
366
            log.info(import_error_msg)
×
367
            return
×
368

369
        if molecules:
×
370
            if species is None:
×
371
                species = list(self.molecules)
×
372
        if species is None:
×
373
            species = list(self.species)
×
374

375
        if config.jupyter:
×
376
            log.info(
×
377
                "ZnVis visualizer currently does not support deployment from "
378
                "jupyter. Please run your analysis as a python script to use"
379
                "the visualizer."
380
            )
381
            return
×
382
        else:
383
            visualizer = SimulationVisualizer(
×
384
                species=species, unwrapped=unwrapped, database_path=self.database_path
385
            )
386
            visualizer.run_visualization()
×
387

388
    # def map_elements(self, mapping: dict = None):
389
    #     """
390
    #     Map numerical keys to element names in the Experiment class and database_path.
391
    #
392
    #     Returns
393
    #     -------
394
    #     Updates the class
395
    #     """
396
    #
397
    #     if mapping is None:
398
    #         log.info("Must provide a mapping")
399
    #         return
400
    #
401
    #     # rename keys in species dictionary
402
    #     for item in mapping:
403
    #         self.species[mapping[item]] = self.species.pop(item)
404
    #
405
    #     # rename database_path groups
406
    #     db_object = Database(name=os.path.join(self.database_path,
407
    #     "database_path.hdf5"))
408
    #     db_object.change_key_names(mapping)
409
    #
410
    #     self.save_class()  # update the class state
411
    #
412
    #
413
    # def set_element(self, old_name, new_name):
414
    #     """
415
    #     Change the name of the element in the self.species dictionary
416
    #
417
    #     Parameters
418
    #     ----------
419
    #     old_name : str
420
    #             Name of the element you want to change
421
    #     new_name : str
422
    #             New name of the element
423
    #     """
424
    #     # Check if the new name is new
425
    #     if new_name != old_name:
426
    #         self.species[new_name] = self.species[old_name]  # update dict
427
    #         del self.species[old_name]  # remove old entry
428

429
    def set_charge(self, element: str, charge: float):
4✔
430
        """
431
        Set the charge/s of an element.
432

433
        Parameters
434
        ----------
435
        element : str
436
                Name of the element whose charge you want to change
437
        charge : list
438
                New charge/s of the element
439
        """
440
        species = self.species
×
441
        species[element].charge = [charge]
×
442
        self.species = species
×
443

444
    def set_mass(self, element: str, mass: float):
4✔
445
        """
446
        Set the mass/es of an element.
447

448
        Parameters
449
        ----------
450
        element : str
451
                Name of the element whose mass you want to change
452
        mass : list
453
                New mass/es of the element
454
        """
455
        species = self.species
×
456
        species[element].mass = mass
×
457
        self.species = species
×
458

459
    def add_data(
4✔
460
        self,
461
        simulation_data: Union[
462
            str, pathlib.Path, mdsuite.file_io.file_read.FileProcessor, list
463
        ],
464
        force: bool = False,
465
        update_with_pubchempy: bool = True,
466
    ):
467
        """
468
        Add data to experiment. This method takes a filename, file path or a file
469
        reader (or a list thereof). If given a filename, it will try to instantiate the
470
        appropriate file reader with its default arguments. If you have a custom data
471
        format with its own reader or want to use non-default arguments for your reader,
472
        instantiate the reader and pass it to this method.
473
        TODO reference online documentation of data loading in the error messages
474
        Parameters
475
        ----------
476
        simulation_data : str or pathlib.Path or mdsuite.file_io.file_read.FileProcessor
477
            or list thereof
478
            if str or pathlib.Path: path to the file that contains the simulation_data
479
            if mdsuite.file_io.file_read.FileProcessor: An already instantiated file
480
            reader from mdsuite.file_io
481
            if list : must be list of any of the above (can be mixed).
482
        force : bool
483
            If true, a file will be read regardless of if it has already been seen.
484
            Default: False
485
        update_with_pubchempy: bool
486
            Whether or not to look for the masses of the species in pubchempy.
487
            Default: True.
488

489
        """
490
        if isinstance(simulation_data, list):
4✔
491
            for elem in simulation_data:
4✔
492
                proc = _get_processor(elem)
4✔
493
                self._add_data_from_file_processor(
4✔
494
                    proc, force=force, update_with_pubchempy=update_with_pubchempy
495
                )
496
        else:
497
            proc = _get_processor(simulation_data)
4✔
498
            self._add_data_from_file_processor(
4✔
499
                proc, force=force, update_with_pubchempy=update_with_pubchempy
500
            )
501

502
    def _add_data_from_file_processor(
4✔
503
        self,
504
        file_processor: mdsuite.file_io.file_read.FileProcessor,
505
        force: bool = False,
506
        update_with_pubchempy: bool = True,
507
    ):
508
        """
509
        Add tensor_values to the database_path.
510

511
        Parameters
512
        ----------
513
        file_processor
514
            The FileProcessor that is able to provide the metadata and the trajectory
515
            to be saved
516
        force : bool
517
                If true, a file will be read regardless of if it has already
518
                been seen.
519
        update_with_pubchempy: bool
520
                Whether or not to look for the masses of the species in pubchempy
521
        """
522
        already_read = str(file_processor) in self.read_files
4✔
523
        if already_read and not force:
4✔
524
            log.info(
4✔
525
                "This file has already been read, skipping this now."
526
                "If this is not desired, please add force=True "
527
                "to the command."
528
            )
529
            return
4✔
530

531
        database = Database(self.database_path / "database.hdf5")
4✔
532

533
        metadata = file_processor.metadata
4✔
534
        architecture = _species_list_to_architecture_dict(
4✔
535
            metadata.species_list, metadata.n_configurations
536
        )
537
        if not database.database_exists():
4✔
538
            self._store_metadata(metadata, update_with_pubchempy=update_with_pubchempy)
4✔
539
            database.initialize_database(architecture)
4✔
540
        else:
541
            database.resize_datasets(architecture)
4✔
542

543
        for i, batch in enumerate(file_processor.get_configurations_generator()):
4✔
544
            database.add_data(chunk=batch, start_idx=self.number_of_configurations)
4✔
545
            self.number_of_configurations += batch.chunk_size
4✔
546

547
        self.version += 1
4✔
548

549
        self.memory_requirements = database.get_memory_information()
4✔
550

551
        # set at the end, because if something fails, the file was not properly read.
552
        self.read_files = self.read_files + [str(file_processor)]
4✔
553

554
    def load_matrix(
4✔
555
        self,
556
        property_name: str = None,
557
        species: typing.Iterable[str] = None,
558
        select_slice: np.s_ = None,
559
        path: typing.Iterable[str] = None,
560
    ):
561
        """
562
        Load a desired property matrix.
563

564
        Parameters
565
        ----------
566
        property_name : str
567
                Name of the matrix to be loaded, e.g. 'Unwrapped_Positions',
568
                'Velocities'
569
        species : Iterable[str]
570
                List of species to be loaded
571
        select_slice : np.slice
572
                A slice to select from the database_path.
573
        path : str
574
                optional path to the database_path.
575

576
        Returns
577
        -------
578
        property_matrix : np.array, tf.Tensor
579
                Tensor of the property to be studied. Format depends on kwargs.
580
        """
581
        database = Database(self.database_path / "database.hdf5")
4✔
582

583
        if path is not None:
4!
584
            return database.load_data(path_list=path, select_slice=select_slice)
×
585

586
        else:
587
            # If no species list is given, use all species in the Experiment.
588
            if species is None:
4!
589
                species = list(self.species.keys())
×
590
            # If no slice is given, load all configurations.
591
            if select_slice is None:
4!
592
                select_slice = np.s_[:]  # set the numpy slice object.
4✔
593

594
        path_list = []
4✔
595
        for item in species:
4✔
596
            path_list.append(join_path(item, property_name))
4✔
597
        return database.load_data(path_list=path_list, select_slice=select_slice)
4✔
598

599
    def _store_metadata(self, metadata: TrajectoryMetadata, update_with_pubchempy=False):
4✔
600
        """Save Metadata in the SQL DB.
601

602
        Parameters
603
        ----------
604
        metadata: TrajectoryMetadata
605
        update_with_pubchempy: bool
606
            Load data from pubchempy and add it to fill missing infomration
607
        """
608
        # new trajectory: store all metadata and construct a new database
609
        self.temperature = metadata.temperature
4✔
610
        self.box_array = metadata.box_l
4✔
611
        if self.box_array is not None:
4✔
612
            self.dimensions = mdsuite.utils.meta_functions.get_dimensionality(
4✔
613
                self.box_array
614
            )
615
        else:
616
            self.dimensions = None
4✔
617
        # todo look into replacing these properties
618
        self.sample_rate = metadata.sample_rate
4✔
619
        species_list = copy.deepcopy(metadata.species_list)
4✔
620
        if update_with_pubchempy:
4!
621
            update_species_attributes_with_pubchempy(species_list)
4✔
622
        # store the species information in dict format
623
        species_dict = {}
4✔
624
        for sp_info in species_list:
4✔
625
            species_dict[sp_info.name] = {
4✔
626
                # look here
627
                "mass": sp_info.mass,
628
                "charge": sp_info.charge,
629
                "n_particles": sp_info.n_particles,
630
                # legacy: calculators use this list to determine the number of particles
631
                # TODO change this.
632
                "indices": list(range(sp_info.n_particles)),
633
                "properties": [prop_info.name for prop_info in sp_info.properties],
634
            }
635
        self.species = species_dict
4✔
636
        # assume the same property for each species
637
        self.property_groups = next(iter(species_dict.values()))["properties"]
4✔
638
        # update n_atoms
639
        self.number_of_atoms = sum(sp["n_particles"] for sp in species_dict.values())
4✔
640

641

642
def update_species_attributes_with_pubchempy(species_list: List[SpeciesInfo]):
4✔
643
    """
644
    Add information to the species dictionary.
645

646
    A fundamental part of this package is species specific analysis. Therefore, the
647
    Pubchempy package is used to add important species specific information to the
648
    class. This will include the charge of the ions which will be used in
649
    conductivity calculations.
650

651
    """
652
    with importlib.resources.open_text(
4✔
653
        "mdsuite.data", "PubChemElements_all.json"
654
    ) as json_file:
655
        pse = json.loads(json_file.read())
4✔
656

657
    # Try to get the species tensor_values from the Periodic System of Elements file
658

659
    # set/find masses
660
    for sp_info in species_list:
4✔
661
        for entry in pse:
4✔
662
            if pse[entry][1] == sp_info.name:
4✔
663
                sp_info.mass = [float(pse[entry][3])]
4✔
664

665
                # If gathering the tensor_values from the PSE file was not successful
666
    # try to get it from Pubchem via pubchempy
667
    for sp_info in species_list:
4✔
668
        if sp_info.mass is None:
4✔
669
            try:
4✔
670
                temp = pcp.get_compounds(sp_info.name, "name")
4✔
671
                temp[0].to_dict(
4✔
672
                    properties=[
673
                        "atoms",
674
                        "bonds",
675
                        "exact_mass",
676
                        "molecular_weight",
677
                        "elements",
678
                    ]
679
                )
680
                sp_info.mass = [temp[0].molecular_weight]
4✔
681
                log.debug(temp[0].exact_mass)
4✔
682
            except (ElementMassAssignedZero, IndexError):
4✔
683
                sp_info.mass = 0.0
4✔
684
                log.warning(f"WARNING element {sp_info.name} has been assigned mass=0.0")
4✔
685
    return species_list
4✔
686

687

688
def _species_list_to_architecture_dict(species_list, n_configurations):
4✔
689
    # TODO let the database handler use the species list directly instead of the dict
690
    """
691
    converter from species list to legacy architecture dict
692
    Parameters
693
    ----------
694
    species_list
695
    n_configurations.
696

697
    Returns
698
    -------
699
    dict like architecture = {'Na':{'Positions':(n_part, n_config, n_dim)}}
700
    """
701
    architecture = {}
4✔
702
    for sp_info in species_list:
4✔
703
        architecture[sp_info.name] = {}
4✔
704
        for prop_info in sp_info.properties:
4✔
705
            architecture[sp_info.name][prop_info.name] = (
4✔
706
                sp_info.n_particles,
707
                n_configurations,
708
                prop_info.n_dims,
709
            )
710
    return architecture
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc