diff --git a/openfisca_core/data_storage/__init__.py b/openfisca_core/data_storage/__init__.py deleted file mode 100644 index e2b4d8911d..0000000000 --- a/openfisca_core/data_storage/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from openfisca_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from openfisca_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from openfisca_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - -from .in_memory_storage import InMemoryStorage # noqa: F401 -from .on_disk_storage import OnDiskStorage # noqa: F401 diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py deleted file mode 100644 index 0808612ba8..0000000000 --- a/openfisca_core/data_storage/in_memory_storage.py +++ /dev/null @@ -1,63 +0,0 @@ -import numpy - -from openfisca_core import periods -from openfisca_core.periods import DateUnit - - -class InMemoryStorage: - """Low-level class responsible for storing and retrieving calculated vectors in memory.""" - - def __init__(self, is_eternal=False) -> None: - self._arrays = {} - self.is_eternal = is_eternal - - def get(self, period): - if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) - period = periods.period(period) - - values = self._arrays.get(period) - if values is None: - return None - return values - - def put(self, value, period) -> None: - if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) - period = periods.period(period) - - self._arrays[period] = value - - def delete(self, period=None) -> None: - if period is None: - self._arrays = {} - return - - if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) - period = periods.period(period) - - self._arrays = { - period_item: value - for period_item, value in self._arrays.items() - if not period.contains(period_item) - } - - def get_known_periods(self): - return self._arrays.keys() - - def get_memory_usage(self): - if not self._arrays: - return { - "nb_arrays": 0, - "total_nb_bytes": 0, - "cell_size": numpy.nan, - } - - nb_arrays = len(self._arrays) - array = next(iter(self._arrays.values())) - return { - "nb_arrays": nb_arrays, - "total_nb_bytes": array.nbytes * nb_arrays, - "cell_size": array.itemsize, - } diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py deleted file mode 100644 index 9133db2376..0000000000 --- a/openfisca_core/data_storage/on_disk_storage.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import shutil - -import numpy - -from openfisca_core import periods -from openfisca_core.indexed_enums import EnumArray -from openfisca_core.periods import DateUnit - - -class OnDiskStorage: - """Low-level class responsible for storing and retrieving calculated vectors on disk.""" - - def __init__( - self, storage_dir, is_eternal=False, preserve_storage_dir=False - ) -> None: - self._files = {} - self._enums = {} - self.is_eternal = is_eternal - self.preserve_storage_dir = preserve_storage_dir - self.storage_dir = storage_dir - - def _decode_file(self, file): - enum = self._enums.get(file) - if enum is not None: - return EnumArray(numpy.load(file), enum) - return numpy.load(file) - - def get(self, period): - if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) - period = periods.period(period) - - values = self._files.get(period) - if values is None: - return None - return self._decode_file(values) - - def put(self, value, period) -> None: - if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) - period = periods.period(period) - - filename = str(period) - path = os.path.join(self.storage_dir, filename) + ".npy" - if isinstance(value, EnumArray): - self._enums[path] = value.possible_values - value = value.view(numpy.ndarray) - numpy.save(path, value) - self._files[period] = path - - def delete(self, period=None) -> None: - if period is None: - self._files = {} - return - - if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) - period = periods.period(period) - - if period is not None: - self._files = { - period_item: value - for period_item, value in self._files.items() - if not period.contains(period_item) - } - - def get_known_periods(self): - return self._files.keys() - - def restore(self) -> None: - self._files = files = {} - # Restore self._files from content of storage_dir. - for filename in os.listdir(self.storage_dir): - if not filename.endswith(".npy"): - continue - path = os.path.join(self.storage_dir, filename) - filename_core = filename.rsplit(".", 1)[0] - period = periods.period(filename_core) - files[period] = path - - def __del__(self) -> None: - if self.preserve_storage_dir: - return - shutil.rmtree(self.storage_dir) # Remove the holder temporary files - # If the simulation temporary directory is empty, remove it - parent_dir = os.path.abspath(os.path.join(self.storage_dir, os.pardir)) - if not os.listdir(parent_dir): - shutil.rmtree(parent_dir) diff --git a/openfisca_core/holders/__init__.py b/openfisca_core/holders/__init__.py index c8422af7d5..f43b503be8 100644 --- a/openfisca_core/holders/__init__.py +++ b/openfisca_core/holders/__init__.py @@ -1,29 +1,35 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from openfisca_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from openfisca_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from openfisca_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - -from .helpers import ( # noqa: F401 - set_input_dispatch_by_period, - set_input_divide_by_period, -) -from .holder import Holder # noqa: F401 -from .memory_usage import MemoryUsage # noqa: F401 +"""Transitional imports to ensure non-breaking changes. + +These imports could be deprecated in the next major release. + +Currently, imports are used in the following way:: + + from openfisca_core.module import symbol + +This example causes cyclic dependency problems, which prevent us from +modularising the different components of the library and make them easier to +test and maintain. + +After the next major release, imports could be used in the following way:: + + from openfisca_core import module + module.symbol() + +And for classes:: + + from openfisca_core.module import Symbol + Symbol() + +.. seealso:: `PEP8#Imports`_ and `OpenFisca's Styleguide`_. + +.. _PEP8#Imports: + https://www.python.org/dev/peps/pep-0008/#imports + +.. _OpenFisca's Styleguide: + https://github.com/openfisca/openfisca-core/blob/master/STYLEGUIDE.md + +""" + +from .helpers import set_input_dispatch_by_period, set_input_divide_by_period +from .holder import Holder +from .repos import DiskRepo, MemoryRepo diff --git a/openfisca_core/holders/helpers/__init__.py b/openfisca_core/holders/helpers/__init__.py new file mode 100644 index 0000000000..86fbf0f9ba --- /dev/null +++ b/openfisca_core/holders/helpers/__init__.py @@ -0,0 +1 @@ +from ._set_input import set_input_dispatch_by_period, set_input_divide_by_period diff --git a/openfisca_core/holders/helpers.py b/openfisca_core/holders/helpers/_set_input.py similarity index 71% rename from openfisca_core/holders/helpers.py rename to openfisca_core/holders/helpers/_set_input.py index fcc6563c79..ae325aa7e2 100644 --- a/openfisca_core/holders/helpers.py +++ b/openfisca_core/holders/helpers/_set_input.py @@ -1,14 +1,21 @@ +from typing import Any + +from openfisca_core.types import Period + import logging import numpy from openfisca_core import periods +from ..holder import Holder + log = logging.getLogger(__name__) -def set_input_dispatch_by_period(holder, period, array) -> None: - """This function can be declared as a ``set_input`` attribute of a variable. +def set_input_dispatch_by_period(holder: Holder, period: Period, array: Any) -> None: + """ + This function can be declared as a ``set_input`` attribute of a variable. In this case, the variable will accept inputs on larger periods that its definition period, and the value for the larger period will be applied to all its subperiods. @@ -19,15 +26,12 @@ def set_input_dispatch_by_period(holder, period, array) -> None: period_size = period.size period_unit = period.unit - if holder.variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar - ): - msg = "set_input_dispatch_by_period can't be used for eternal variables." + if holder.eternal: raise ValueError( - msg, + "set_input_dispatch_by_period can't be used for eternal variables." ) - cached_period_unit = holder.variable.definition_period + cached_period_unit = holder.period after_instant = period.start.offset(period_size, period_unit) # Cache the input data, skipping the existing cached months @@ -43,8 +47,9 @@ def set_input_dispatch_by_period(holder, period, array) -> None: sub_period = sub_period.offset(1) -def set_input_divide_by_period(holder, period, array) -> None: - """This function can be declared as a ``set_input`` attribute of a variable. +def set_input_divide_by_period(holder: Holder, period: Period, array: Any) -> None: + """ + This function can be declared as a ``set_input`` attribute of a variable. In this case, the variable will accept inputs on larger periods that its definition period, and the value for the larger period will be divided between its subperiods. @@ -55,15 +60,12 @@ def set_input_divide_by_period(holder, period, array) -> None: period_size = period.size period_unit = period.unit - if holder.variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar - ): - msg = "set_input_divide_by_period can't be used for eternal variables." + if holder.eternal: raise ValueError( - msg, + "set_input_divide_by_period can't be used for eternal variables." ) - cached_period_unit = holder.variable.definition_period + cached_period_unit = holder.period after_instant = period.start.offset(period_size, period_unit) # Count the number of elementary periods to change, and the difference with what is already known. @@ -87,7 +89,8 @@ def set_input_divide_by_period(holder, period, array) -> None: holder._set(sub_period, divided_array) sub_period = sub_period.offset(1) elif not (remaining_array == 0).all(): - msg = f"Inconsistent input: variable {holder.variable.name} has already been set for all months contained in period {period}, and value {array} provided for {period} doesn't match the total ({array - remaining_array}). This error may also be thrown if you try to call set_input twice for the same variable and period." raise ValueError( - msg, + "Inconsistent input: variable {0} has already been set for all months contained in period {1}, and value {2} provided for {1} doesn't match the total ({3}). This error may also be thrown if you try to call set_input twice for the same variable and period.".format( + holder.variable.name, period, array, array - remaining_array + ) ) diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index 7183d4a44a..33c00d7096 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -2,51 +2,123 @@ from collections.abc import Sequence from typing import Any +from typing_extensions import Literal +from openfisca_core.types import Period, Population, Simulation, Variable + +import itertools import os import warnings import numpy import psutil +from sortedcontainers import sorteddict from openfisca_core import ( commons, - data_storage as storage, errors, + experimental, indexed_enums as enums, periods, - types, + tools, ) -from .memory_usage import MemoryUsage +from .repos import DiskRepo, MemoryRepo +from .typing import MemoryUsage, Storage class Holder: - """A holder keeps tracks of a variable values after they have been calculated, or set as an input.""" + """Caches calculated or input variable values.""" - def __init__(self, variable, population) -> None: - self.population = population + variable: Variable + population: Population + simulation: Simulation + __stores__: dict[Literal["memory", "disk"], Storage] + + def __init__(self, variable: Variable, population: Population) -> None: self.variable = variable + self.population = population self.simulation = population.simulation - self._eternal = self.variable.definition_period == periods.DateUnit.ETERNITY - self._memory_storage = storage.InMemoryStorage(is_eternal=self._eternal) - - # By default, do not activate on-disk storage, or variable dropping - self._disk_storage = None - self._on_disk_storable = False - self._do_not_store = False - if self.simulation and self.simulation.memory_config: - if ( - self.variable.name - not in self.simulation.memory_config.priority_variables - ): - self._disk_storage = self.create_disk_storage() - self._on_disk_storable = True - if self.variable.name in self.simulation.memory_config.variables_to_drop: - self._do_not_store = True + + if self.storable: + self.stores = sorteddict.SortedDict( + { + "memory": MemoryRepo(), + "disk": self.create_disk_repo(), + } + ) + + else: + self.stores = sorteddict.SortedDict( + { + "memory": MemoryRepo(), + } + ) + + @property + def name(self) -> str: + return self.variable.name + + @property + def period(self) -> Period: + return self.variable.definition_period + + @property + def eternal(self) -> bool: + return self.period == periods.DateUnit.ETERNITY + + @property + def neutralised(self) -> bool: + return self.variable.is_neutralized + + @property + def config(self) -> experimental.MemoryConfig | None: + try: + return self.simulation.memory_config + + except AttributeError: + return None + + @property + def durable(self) -> bool: + return bool(self.config) + + @property + def transient(self) -> bool: + return not self.durable + + @property + def storable(self) -> bool: + if self.transient: + return False + + if self.config is None: + return False + + return self.name not in self.config.priority_variables + + @property + def cacheable(self) -> bool: + if self.transient: + return False + + if self.config is None: + return False + + return self.name not in self.config.variables_to_drop + + @property + def stores(self) -> dict[Literal["memory", "disk"], Storage]: + return self.__stores__ + + @stores.setter + def stores(self, stores: dict[Literal["memory", "disk"], Storage]) -> None: + self.__stores__ = stores def clone(self, population): - """Copy the holder just enough to be able to run a new simulation without modifying the original simulation.""" + """ + Copies the holder just enough to be able to run a new simulation without modifying the original simulation. + """ new = commons.empty_clone(self) new_dict = new.__dict__ @@ -59,43 +131,64 @@ def clone(self, population): return new - def create_disk_storage(self, directory=None, preserve=False): + def create_disk_repo( + self, + directory: str | None = None, + preserve: bool = False, + ) -> Storage: if directory is None: directory = self.simulation.data_storage_dir - storage_dir = os.path.join(directory, self.variable.name) + + storage_dir = os.path.join(directory, self.name) + if not os.path.isdir(storage_dir): os.mkdir(storage_dir) - return storage.OnDiskStorage( - storage_dir, - self._eternal, - preserve_storage_dir=preserve, - ) - def delete_arrays(self, period=None) -> None: - """If ``period`` is ``None``, remove all known values of the variable. + return DiskRepo(storage_dir, preserve) + + def delete_arrays(self, period: Any = None) -> None: + """ + If ``period`` is ``None``, remove all known values of the variable. If ``period`` is not ``None``, only remove all values for any period included in period (e.g. if period is "2017", values for "2017-01", "2017-07", etc. would be removed) """ - self._memory_storage.delete(period) - if self._disk_storage: - self._disk_storage.delete(period) - def get_array(self, period): - """Get the value of the variable for the given period. + if self.eternal and period is not None: + period = periods.period(periods.DateUnit.ETERNITY) + + else: + period = periods.period(period) + + for store in self.stores.values(): + store.delete(period) + + return None + + def get_array(self, period: Period) -> numpy.ndarray | None: + """ + Gets the value of the variable for the given period. If the value is not known, return ``None``. """ - if self.variable.is_neutralized: - return self.default_array() - value = self._memory_storage.get(period) - if value is not None: - return value - if self._disk_storage: - return self._disk_storage.get(period) + if self.neutralised: + return self.variable.default_array(self.population.count) + + if self.eternal: + period = periods.period(periods.DateUnit.ETERNITY) + + else: + period = periods.period(period) + + for store in self.stores.values(): + value = store.get(period) + + if value is not None: + return value + return None def get_memory_usage(self) -> MemoryUsage: - """Get data about the virtual memory usage of the Holder. + """Gets data about the virtual memory usage of the Holder. Returns: Memory usage data. @@ -109,7 +202,7 @@ def get_memory_usage(self) -> MemoryUsage: ... simulations, ... taxbenefitsystems, ... variables, - ... ) + ... ) >>> entity = entities.Entity("", "", "", "") @@ -127,7 +220,7 @@ def get_memory_usage(self) -> MemoryUsage: >>> simulation = simulations.Simulation(tbs, entities) >>> holder.simulation = simulation - >>> pprint(holder.get_memory_usage(), indent=3) + >>> pprint(holder.get_memory_usage(), indent = 3) { 'cell_size': nan, 'dtype': , 'nb_arrays': 0, @@ -135,40 +228,44 @@ def get_memory_usage(self) -> MemoryUsage: 'total_nb_bytes': 0... """ + usage = MemoryUsage( nb_cells_by_array=self.population.count, dtype=self.variable.dtype, ) - usage.update(self._memory_storage.get_memory_usage()) + usage.update(self.stores["memory"].usage()) if self.simulation.trace: - nb_requests = self.simulation.tracer.get_nb_requests(self.variable.name) + nb_requests = self.simulation.tracer.get_nb_requests(self.name) usage.update( - { - "nb_requests": nb_requests, - "nb_requests_by_array": ( + dict( + nb_requests=nb_requests, + nb_requests_by_array=( nb_requests / float(usage["nb_arrays"]) if usage["nb_arrays"] > 0 else numpy.nan ), - }, + ) ) return usage - def get_known_periods(self): - """Get the list of periods the variable value is known for.""" - return list(self._memory_storage.get_known_periods()) + list( - self._disk_storage.get_known_periods() if self._disk_storage else [], - ) + def get_known_periods(self) -> Sequence[Period]: + """ + Gets the list of periods the variable value is known for. + """ + + known_periods = (store.periods() for store in self.stores.values()) + + return list(itertools.chain(*known_periods)) def set_input( self, - period: types.Period, + period: Period, array: numpy.ndarray | Sequence[Any], ) -> numpy.ndarray | None: - """Set a Variable's array of values of a given Period. + """Sets a Variable's array of values of a given Period. Args: period: The period at which the value is set. @@ -185,7 +282,6 @@ def set_input( Examples: >>> from openfisca_core import entities, populations, variables - >>> entity = entities.Entity("", "", "", "") >>> class MyVariable(variables.Variable): @@ -211,70 +307,89 @@ def set_input( https://openfisca.org/doc/coding-the-legislation/35_periods.html#set-input-automatically-process-variable-inputs-defined-for-periods-not-matching-the-definition-period """ + period = periods.period(period) - if period.unit == periods.DateUnit.ETERNITY and not self._eternal: + if period.unit == periods.DateUnit.ETERNITY and not self.eternal: error_message = os.linesep.join( [ - "Unable to set a value for variable {1} for {0}.", - "{1} is only defined for {2}s. Please adapt your input.", - ], - ).format( - periods.DateUnit.ETERNITY.upper(), - self.variable.name, - self.variable.definition_period, + f"Unable to set a value for variable {self.name} for {periods.DateUnit.ETERNITY.upper()}.", + f"{self.name} is only defined for {self.period}s. Please adapt your input.", + ] ) + raise errors.PeriodMismatchError( - self.variable.name, - period, - self.variable.definition_period, - error_message, + self.name, period, self.period, error_message + ) + + if self.neutralised: + warning_message = f"You cannot set a value for the variable {self.name}, as it has been neutralized. The value you provided ({array}) will be ignored." + return warnings.warn( + warning_message, + Warning, + stacklevel=2, ) - if self.variable.is_neutralized: - warning_message = f"You cannot set a value for the variable {self.variable.name}, as it has been neutralized. The value you provided ({array}) will be ignored." - return warnings.warn(warning_message, Warning, stacklevel=2) + if self.variable.value_type in (float, int) and isinstance(array, str): - array = commons.eval_expression(array) + array = tools.eval_expression(array) + if self.variable.set_input: return self.variable.set_input(self, period, array) - return self._set(period, array) - def _to_array(self, value): + self._set(period, array) + + return None + + def _to_array(self, value: Any) -> numpy.ndarray: if not isinstance(value, numpy.ndarray): value = numpy.asarray(value) + if value.ndim == 0: # 0-dim arrays are casted to scalar when they interact with float. We don't want that. value = value.reshape(1) + if len(value) != self.population.count: - msg = f'Unable to set value "{value}" for variable "{self.variable.name}", as its length is {len(value)} while there are {self.population.count} {self.population.entity.plural} in the simulation.' raise ValueError( - msg, + 'Unable to set value "{}" for variable "{}", as its length is {} while there are {} {} in the simulation.'.format( + value, + self.name, + len(value), + self.population.count, + self.population.entity.plural, + ) ) + if self.variable.value_type == enums.Enum: value = self.variable.possible_values.encode(value) + if value.dtype != self.variable.dtype: try: value = value.astype(self.variable.dtype) + except ValueError: - msg = f'Unable to set value "{value}" for variable "{self.variable.name}", as the variable dtype "{self.variable.dtype}" does not match the value dtype "{value.dtype}".' raise ValueError( - msg, + 'Unable to set value "{}" for variable "{}", as the variable dtype "{}" does not match the value dtype "{}".'.format( + value, self.name, self.variable.dtype, value.dtype + ) ) + return value - def _set(self, period, value) -> None: + def _set(self, period: Period | None, value: numpy.ndarray | Sequence[Any]) -> None: value = self._to_array(value) - if not self._eternal: + + if self.eternal: + period = periods.period(periods.DateUnit.ETERNITY) + + else: if period is None: - msg = ( + raise ValueError( f"A period must be specified to set values, except for variables with " f"{periods.DateUnit.ETERNITY.upper()} as as period_definition." ) - raise ValueError( - msg, - ) - if self.variable.definition_period != period.unit or period.size > 1: - name = self.variable.name + + if self.period != period.unit or period.size > 1: + name = self.name period_size_adj = ( f"{period.unit}" if (period.size == 1) @@ -283,43 +398,39 @@ def _set(self, period, value) -> None: error_message = os.linesep.join( [ f'Unable to set a value for variable "{name}" for {period_size_adj}-long period "{period}".', - f'"{name}" can only be set for one {self.variable.definition_period} at a time. Please adapt your input.', + f'"{name}" can only be set for one {self.period} at a time. Please adapt your input.', f'If you are the maintainer of "{name}", you can consider adding it a set_input attribute to enable automatic period casting.', - ], + ] ) raise errors.PeriodMismatchError( - self.variable.name, - period, - self.variable.definition_period, - error_message, + self.name, period, self.period, error_message ) should_store_on_disk = ( - self._on_disk_storable - and self._memory_storage.get(period) is None - and psutil.virtual_memory().percent # If there is already a value in memory, replace it and don't put a new value in the disk storage + self.storable + and self.stores["memory"].get(period) is None + and psutil.virtual_memory().percent + # If there is already a value in memory, replace it + # and don't put a new value in the disk storage >= self.simulation.memory_config.max_memory_occupation_pc ) if should_store_on_disk: - self._disk_storage.put(value, period) + self.stores["disk"].put(value, period) + else: - self._memory_storage.put(value, period) + self.stores["memory"].put(value, period) - def put_in_cache(self, value, period) -> None: - if self._do_not_store: - return + def put_in_cache(self, value: numpy.ndarray, period: Period) -> None: + if not self.transient and not self.cacheable: + return None if ( self.simulation.opt_out_cache and self.simulation.tax_benefit_system.cache_blacklist - and self.variable.name in self.simulation.tax_benefit_system.cache_blacklist + and self.name in self.simulation.tax_benefit_system.cache_blacklist ): return self._set(period, value) - - def default_array(self): - """Return a new array of the appropriate length for the entity, filled with the variable default values.""" - return self.variable.default_array(self.population.count) diff --git a/openfisca_core/holders/memory_usage.py b/openfisca_core/holders/memory_usage.py deleted file mode 100644 index 2d344318ee..0000000000 --- a/openfisca_core/holders/memory_usage.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing_extensions import TypedDict - -import numpy - - -class MemoryUsage(TypedDict, total=False): - """Virtual memory usage of a Holder. - - Attributes: - cell_size: The amount of bytes assigned to each value. - dtype: The :mod:`numpy.dtype` of any, each, and every value. - nb_arrays: The number of periods for which the Holder contains values. - nb_cells_by_array: The number of entities in the current Simulation. - nb_requests: The number of times the Variable has been computed. - nb_requests_by_array: Average times a stored array has been read. - total_nb_bytes: The total number of bytes used by the Holder. - - """ - - cell_size: int - dtype: numpy.dtype - nb_arrays: int - nb_cells_by_array: int - nb_requests: int - nb_requests_by_array: int - total_nb_bytes: int diff --git a/openfisca_core/holders/py.typed b/openfisca_core/holders/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/holders/repos/__init__.py b/openfisca_core/holders/repos/__init__.py new file mode 100644 index 0000000000..e54b6d9f98 --- /dev/null +++ b/openfisca_core/holders/repos/__init__.py @@ -0,0 +1,2 @@ +from ._disk_repo import DiskRepo +from ._memory_repo import MemoryRepo diff --git a/openfisca_core/holders/repos/_disk_repo.py b/openfisca_core/holders/repos/_disk_repo.py new file mode 100644 index 0000000000..57c24297d0 --- /dev/null +++ b/openfisca_core/holders/repos/_disk_repo.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any, NoReturn + +from openfisca_core.types import Enum, Period + +import os +import pathlib +import shutil + +import numpy + +from openfisca_core import indexed_enums as enums, periods + + +class DiskRepo: + """Class responsible for storing/retrieving vectors on/from disk. + + Attributes: + directory: Path to store calculated vectors. + keep: Flag indicating if folders should be preserved. + + """ + + is_eternal: bool + directory: str + keep: bool + + #: Mapping of file paths to possible Enum values. + __enums__: dict[str, type[Enum]] = {} + + #: Mapping of periods to file paths for stored vectors. + __files__: dict[Period, str] = {} + + def __init__(self, directory: str, keep: bool = False) -> None: + self.directory = directory + self.keep = keep + + def get( + self, + period: Period, + ) -> numpy.ndarray | enums.EnumArray | None: + """Retrieve the data for the specified period from disk. + + Args: + period: The period for which data should be retrieved. + + Returns: + A NumPy array or EnumArray object representing the vector for the + specified period, or None if no vector is stored for that period. + + Examples: + >>> import tempfile + + >>> value = numpy.array([1, 2, 3]) + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.put(value, period) + ... storage.get(period) + array([1, 2, 3]) + + """ + + values = self.__files__.get(period) + + if values is None: + return None + + return self._decode_file(values) + + def put(self, value: Any, period: Period) -> None: + """Store the specified data on disk for the specified period. + + Args: + value: The data to store + period: The period for which the data should be stored. + + Examples: + >>> import tempfile + + >>> value = numpy.array([1, 2, 3]) + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.put(value, period) + ... storage.get(period) + array([1, 2, 3]) + + """ + + stem = str(period) + path = os.path.join(self.directory, f"{stem}.npy") + + if isinstance(value, enums.EnumArray): + self.__enums__ = {path: value.possible_values, **self.__enums__} + value = value.view(numpy.ndarray) + + numpy.save(path, value) + self.__files__ = {period: path, **self.__files__} + + def delete(self, period: Period | None = None) -> None: + """Delete the data for the specified period from disk. + + Args: + period: The period for which data should be deleted. If not + specified, all data will be deleted. + + Examples: + >>> import tempfile + + >>> value = numpy.array([1, 2, 3]) + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.put(value, period) + ... storage.get(period) + array([1, 2, 3]) + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.put(value, period) + ... storage.delete(period) + ... storage.get(period) + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.put(value, period) + ... storage.delete() + ... storage.get(period) + + """ + + if period is None: + self.__files__ = {} + return None + + self.__files__ = { + key: value + for key, value in self.__files__.items() + if not period.contains(key) + } + + def periods(self) -> Sequence[Period]: + """List of storage's known periods. + + Returns: + A sequence containing the storage's known periods. + + Examples: + >>> import tempfile + + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.periods() + [] + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.put([], period) + ... storage.periods() + [Period(('year', Instant((2017, 1, 1)), 1))] + + """ + + return list(self.__files__.keys()) + + def usage(self) -> NoReturn: + """Memory usage of the storage. + + Raises: + NotImplementedError: Method not implemented for this storage. + + Examples: + >>> import tempfile + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.usage() + Traceback (most recent call last): + ... + NotImplementedError: Method not implemented for this storage. + + .. versionadded:: 37.1.0 + + """ + + raise NotImplementedError("Method not implemented for this storage.") + + def restore(self) -> None: + self.__files__ = {} + # Restore self.__files__ from content of directory. + for filename in os.listdir(self.directory): + if not filename.endswith(".npy"): + continue + path = os.path.join(self.directory, filename) + filename_core = filename.rsplit(".", 1)[0] + period = periods.period(filename_core) + self.__files__ = {period: path, **self.__files__} + + def __del__(self) -> None: + if self.keep: + return None + + path = pathlib.Path(self.directory) + + if path.exists(): + # Remove the holder temporary files + shutil.rmtree(self.directory) + + # If the simulation temporary directory is empty, remove it + parent_dir = os.path.abspath(os.path.join(self.directory, os.pardir)) + + if not os.listdir(parent_dir): + shutil.rmtree(parent_dir) + + def _decode_file(self, file: str) -> Any: + """Decodes a file by loading its contents as a NumPy array. + + If the file is associated with Enum values, the array is converted back + to an EnumArray object. + + Args: + file: Path to the file to be decoded. + + Returns: + NumPy array or EnumArray object representing the data in the file. + + Examples + >>> import tempfile + + >>> class Housing(enums.Enum): + ... OWNER = "Owner" + ... TENANT = "Tenant" + ... FREE_LODGER = "Free lodger" + ... HOMELESS = "Homeless" + + >>> array = numpy.array([1]) + >>> value = enums.EnumArray(array, Housing) + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + + >>> with tempfile.TemporaryDirectory() as directory: + ... storage = DiskRepo(directory) + ... storage.put(value, period) + ... storage._decode_file(storage.__files__[period]) + EnumArray([]) + + """ + + enum = self.__enums__.get(file) + load = numpy.load(file) + + if enum is None: + return load + + return enums.EnumArray(load, enum) diff --git a/openfisca_core/holders/repos/_memory_repo.py b/openfisca_core/holders/repos/_memory_repo.py new file mode 100644 index 0000000000..0f84063cd5 --- /dev/null +++ b/openfisca_core/holders/repos/_memory_repo.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +from collections.abc import Sequence +from openfisca_core.holders.typing import MemoryUsage + +from openfisca_core.types import Period + +import numpy + +from openfisca_core import periods + + +class MemoryRepo: + """Class responsible for storing/retrieving vectors in/from memory.""" + + #: A dictionary containing data that has been stored in memory. + __arrays__: dict[Period, numpy.ndarray] = {} + + def get(self, period: Period) -> numpy.ndarray | None: + """Retrieve the data for the specified period from memory. + + Args: + period: The period for which data should be retrieved. + + Returns: + The data for the specified period, or None if no data is available. + + Examples: + >>> storage = MemoryRepo() + >>> value = numpy.array([1, 2, 3]) + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + + >>> storage.put(value, period) + + >>> storage.get(period) + array([1, 2, 3]) + + """ + + values = self.__arrays__.get(period) + + if values is None: + return None + + return values + + def put(self, value: numpy.ndarray, period: Period) -> None: + """Store the specified data in memory for the specified period. + + Args: + value: The data to store + period: The period for which the data should be stored. + + Examples: + >>> storage = MemoryRepo() + >>> value = numpy.array([1, 2, 3]) + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + + >>> storage.put(value, period) + + >>> storage.get(period) + array([1, 2, 3]) + + """ + + self.__arrays__ = {period: value, **self.__arrays__} + + def delete(self, period: Period | None = None) -> None: + """Delete the data for the specified period from memory. + + Args: + period: The period for which data should be deleted. If not + specified, all data will be deleted. + + Examples: + >>> storage = MemoryRepo() + >>> value = numpy.array([1, 2, 3]) + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + + >>> storage.put(value, period) + + >>> storage.get(period) + array([1, 2, 3]) + + >>> storage.delete(period) + + >>> storage.get(period) + + >>> storage.put(value, period) + + >>> storage.delete() + + >>> storage.get(period) + + """ + + if period is None: + self.__arrays__ = {} + return None + + self.__arrays__ = { + key: value + for key, value in self.__arrays__.items() + if not period.contains(key) + } + + def periods(self) -> Sequence[Period]: + """List of storage's known periods. + + Returns: + A sequence containing the storage's known periods. + + Examples: + >>> storage = MemoryRepo() + + >>> storage.periods() + [] + + >>> instant = periods.Instant((2017, 1, 1)) + >>> period = periods.Period(("year", instant, 1)) + >>> storage.put([], period) + >>> storage.periods() + [Period(('year', Instant((2017, 1, 1)), 1))] + + """ + + return list(self.__arrays__.keys()) + + def usage(self) -> MemoryUsage: + """Memory usage of the storage. + + Returns: + A dictionary representing the storage's memory usage. + + Examples: + >>> storage = MemoryRepo() + + >>> storage.usage() + {'cell_size': nan, 'nb_arrays': 0, 'total_nb_bytes': 0} + + """ + + if not self.__arrays__: + return MemoryUsage( + cell_size=numpy.nan, + nb_arrays=0, + total_nb_bytes=0, + ) + + nb_arrays = len(self.__arrays__) + array = next(iter(self.__arrays__.values())) + total = array.nbytes * nb_arrays + + return MemoryUsage( + cell_size=array.itemsize, + nb_arrays=nb_arrays, + total_nb_bytes=total, + ) diff --git a/openfisca_core/holders/tests/test_helpers.py b/openfisca_core/holders/tests/test_helpers.py index 948f25288f..ea6e23708a 100644 --- a/openfisca_core/holders/tests/test_helpers.py +++ b/openfisca_core/holders/tests/test_helpers.py @@ -1,16 +1,11 @@ import pytest -from openfisca_core import holders, tools -from openfisca_core.entities import Entity -from openfisca_core.holders import Holder -from openfisca_core.periods import DateUnit, Instant, Period -from openfisca_core.populations import Population -from openfisca_core.variables import Variable +from openfisca_core import entities, holders, periods, populations, tools, variables @pytest.fixture def people(): - return Entity( + return entities.Entity( key="person", plural="people", label="An individual member of a larger group.", @@ -22,46 +17,51 @@ def people(): def Income(people): return type( "Income", - (Variable,), + (variables.Variable,), {"value_type": float, "entity": people}, ) @pytest.fixture def population(people): - population = Population(people) + population = populations.Population(people) population.count = 1 return population @pytest.mark.parametrize( - ("dispatch_unit", "definition_unit", "values", "expected"), + "dispatch_unit, definition_unit, values, expected", [ - (DateUnit.YEAR, DateUnit.YEAR, [1.0], [3.0]), - (DateUnit.YEAR, DateUnit.MONTH, [1.0], [36.0]), - (DateUnit.YEAR, DateUnit.DAY, [1.0], [1096.0]), - (DateUnit.YEAR, DateUnit.WEEK, [1.0], [157.0]), - (DateUnit.YEAR, DateUnit.WEEKDAY, [1.0], [1096.0]), - (DateUnit.MONTH, DateUnit.YEAR, [1.0], [1.0]), - (DateUnit.MONTH, DateUnit.MONTH, [1.0], [3.0]), - (DateUnit.MONTH, DateUnit.DAY, [1.0], [90.0]), - (DateUnit.MONTH, DateUnit.WEEK, [1.0], [13.0]), - (DateUnit.MONTH, DateUnit.WEEKDAY, [1.0], [90.0]), - (DateUnit.DAY, DateUnit.YEAR, [1.0], [1.0]), - (DateUnit.DAY, DateUnit.MONTH, [1.0], [1.0]), - (DateUnit.DAY, DateUnit.DAY, [1.0], [3.0]), - (DateUnit.DAY, DateUnit.WEEK, [1.0], [1.0]), - (DateUnit.DAY, DateUnit.WEEKDAY, [1.0], [3.0]), - (DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]), - (DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]), - (DateUnit.WEEK, DateUnit.DAY, [1.0], [21.0]), - (DateUnit.WEEK, DateUnit.WEEK, [1.0], [3.0]), - (DateUnit.WEEK, DateUnit.WEEKDAY, [1.0], [21.0]), - (DateUnit.WEEKDAY, DateUnit.YEAR, [1.0], [1.0]), - (DateUnit.WEEKDAY, DateUnit.MONTH, [1.0], [1.0]), - (DateUnit.WEEKDAY, DateUnit.DAY, [1.0], [3.0]), - (DateUnit.WEEKDAY, DateUnit.WEEK, [1.0], [1.0]), - (DateUnit.WEEKDAY, DateUnit.WEEKDAY, [1.0], [3.0]), + [periods.DateUnit.YEAR, periods.DateUnit.YEAR, [1.0], [3.0]], + [periods.DateUnit.YEAR, periods.DateUnit.MONTH, [1.0], [36.0]], + [periods.DateUnit.YEAR, periods.DateUnit.DAY, [1.0], [1096.0]], + [periods.DateUnit.YEAR, periods.DateUnit.WEEK, [1.0], [157.0]], + [periods.DateUnit.YEAR, periods.DateUnit.WEEKDAY, [1.0], [1096.0]], + [periods.DateUnit.MONTH, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.MONTH, periods.DateUnit.MONTH, [1.0], [3.0]], + [periods.DateUnit.MONTH, periods.DateUnit.DAY, [1.0], [90.0]], + [periods.DateUnit.MONTH, periods.DateUnit.WEEK, [1.0], [13.0]], + [periods.DateUnit.MONTH, periods.DateUnit.WEEKDAY, [1.0], [90.0]], + [periods.DateUnit.DAY, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.DAY, periods.DateUnit.MONTH, [1.0], [1.0]], + [periods.DateUnit.DAY, periods.DateUnit.DAY, [1.0], [3.0]], + [periods.DateUnit.DAY, periods.DateUnit.WEEK, [1.0], [1.0]], + [periods.DateUnit.DAY, periods.DateUnit.WEEKDAY, [1.0], [3.0]], + [periods.DateUnit.WEEK, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.MONTH, [1.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.DAY, [1.0], [21.0]], + [periods.DateUnit.WEEK, periods.DateUnit.WEEK, [1.0], [3.0]], + [periods.DateUnit.WEEK, periods.DateUnit.WEEKDAY, [1.0], [21.0]], + [periods.DateUnit.WEEK, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.MONTH, [1.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.DAY, [1.0], [21.0]], + [periods.DateUnit.WEEK, periods.DateUnit.WEEK, [1.0], [3.0]], + [periods.DateUnit.WEEK, periods.DateUnit.WEEKDAY, [1.0], [21.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.MONTH, [1.0], [1.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.DAY, [1.0], [3.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.WEEK, [1.0], [1.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.WEEKDAY, [1.0], [3.0]], ], ) def test_set_input_dispatch_by_period( @@ -71,12 +71,12 @@ def test_set_input_dispatch_by_period( definition_unit, values, expected, -) -> None: +): Income.definition_period = definition_unit income = Income() - holder = Holder(income, population) - instant = Instant((2022, 1, 1)) - dispatch_period = Period((dispatch_unit, instant, 3)) + holder = holders.Holder(income, population) + instant = periods.Instant((2022, 1, 1)) + dispatch_period = periods.Period((dispatch_unit, instant, 3)) holders.set_input_dispatch_by_period(holder, dispatch_period, values) total = sum(map(holder.get_array, holder.get_known_periods())) @@ -85,33 +85,33 @@ def test_set_input_dispatch_by_period( @pytest.mark.parametrize( - ("divide_unit", "definition_unit", "values", "expected"), + "divide_unit, definition_unit, values, expected", [ - (DateUnit.YEAR, DateUnit.YEAR, [3.0], [1.0]), - (DateUnit.YEAR, DateUnit.MONTH, [36.0], [1.0]), - (DateUnit.YEAR, DateUnit.DAY, [1095.0], [1.0]), - (DateUnit.YEAR, DateUnit.WEEK, [157.0], [1.0]), - (DateUnit.YEAR, DateUnit.WEEKDAY, [1095.0], [1.0]), - (DateUnit.MONTH, DateUnit.YEAR, [1.0], [1.0]), - (DateUnit.MONTH, DateUnit.MONTH, [3.0], [1.0]), - (DateUnit.MONTH, DateUnit.DAY, [90.0], [1.0]), - (DateUnit.MONTH, DateUnit.WEEK, [13.0], [1.0]), - (DateUnit.MONTH, DateUnit.WEEKDAY, [90.0], [1.0]), - (DateUnit.DAY, DateUnit.YEAR, [1.0], [1.0]), - (DateUnit.DAY, DateUnit.MONTH, [1.0], [1.0]), - (DateUnit.DAY, DateUnit.DAY, [3.0], [1.0]), - (DateUnit.DAY, DateUnit.WEEK, [1.0], [1.0]), - (DateUnit.DAY, DateUnit.WEEKDAY, [3.0], [1.0]), - (DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]), - (DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]), - (DateUnit.WEEK, DateUnit.DAY, [21.0], [1.0]), - (DateUnit.WEEK, DateUnit.WEEK, [3.0], [1.0]), - (DateUnit.WEEK, DateUnit.WEEKDAY, [21.0], [1.0]), - (DateUnit.WEEKDAY, DateUnit.YEAR, [1.0], [1.0]), - (DateUnit.WEEKDAY, DateUnit.MONTH, [1.0], [1.0]), - (DateUnit.WEEKDAY, DateUnit.DAY, [3.0], [1.0]), - (DateUnit.WEEKDAY, DateUnit.WEEK, [1.0], [1.0]), - (DateUnit.WEEKDAY, DateUnit.WEEKDAY, [3.0], [1.0]), + [periods.DateUnit.YEAR, periods.DateUnit.YEAR, [3.0], [1.0]], + [periods.DateUnit.YEAR, periods.DateUnit.MONTH, [36.0], [1.0]], + [periods.DateUnit.YEAR, periods.DateUnit.DAY, [1095.0], [1.0]], + [periods.DateUnit.YEAR, periods.DateUnit.WEEK, [157.0], [1.0]], + [periods.DateUnit.YEAR, periods.DateUnit.WEEKDAY, [1095.0], [1.0]], + [periods.DateUnit.MONTH, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.MONTH, periods.DateUnit.MONTH, [3.0], [1.0]], + [periods.DateUnit.MONTH, periods.DateUnit.DAY, [90.0], [1.0]], + [periods.DateUnit.MONTH, periods.DateUnit.WEEK, [13.0], [1.0]], + [periods.DateUnit.MONTH, periods.DateUnit.WEEKDAY, [90.0], [1.0]], + [periods.DateUnit.DAY, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.DAY, periods.DateUnit.MONTH, [1.0], [1.0]], + [periods.DateUnit.DAY, periods.DateUnit.DAY, [3.0], [1.0]], + [periods.DateUnit.DAY, periods.DateUnit.WEEK, [1.0], [1.0]], + [periods.DateUnit.DAY, periods.DateUnit.WEEKDAY, [3.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.MONTH, [1.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.DAY, [21.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.WEEK, [3.0], [1.0]], + [periods.DateUnit.WEEK, periods.DateUnit.WEEKDAY, [21.0], [1.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.YEAR, [1.0], [1.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.MONTH, [1.0], [1.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.DAY, [3.0], [1.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.WEEK, [1.0], [1.0]], + [periods.DateUnit.WEEKDAY, periods.DateUnit.WEEKDAY, [3.0], [1.0]], ], ) def test_set_input_divide_by_period( @@ -121,12 +121,12 @@ def test_set_input_divide_by_period( definition_unit, values, expected, -) -> None: +): Income.definition_period = definition_unit income = Income() - holder = Holder(income, population) - instant = Instant((2022, 1, 1)) - divide_period = Period((divide_unit, instant, 3)) + holder = holders.Holder(income, population) + instant = periods.Instant((2022, 1, 1)) + divide_period = periods.Period((divide_unit, instant, 3)) holders.set_input_divide_by_period(holder, divide_period, values) last = holder.get_array(holder.get_known_periods()[-1]) diff --git a/openfisca_core/holders/typing.py b/openfisca_core/holders/typing.py new file mode 100644 index 0000000000..2f08c3d73a --- /dev/null +++ b/openfisca_core/holders/typing.py @@ -0,0 +1,59 @@ +# pylint: disable=missing-class-docstring,missing-function-docstring + +from __future__ import annotations + +from typing import Any +from typing_extensions import Protocol, TypedDict + +import abc + +import numpy + + +class Holder(Protocol): + @abc.abstractmethod + def clone(self, population: Any) -> Holder: ... + + @abc.abstractmethod + def get_memory_usage(self) -> Any: ... + + +class Storage(Protocol): + @abc.abstractmethod + def get(self, period: Any) -> Any: ... + + @abc.abstractmethod + def put(self, values: Any, period: Any) -> None: ... + + @abc.abstractmethod + def delete(self, period: Any = None) -> None: ... + + def periods(self) -> Any: ... + + @abc.abstractmethod + def usage(self) -> Any: ... + + +class MemoryUsage(TypedDict, total=False): + """Virtual memory usage of a storage.""" + + #: The amount of bytes assigned to each value. + cell_size: float + + #: The :mod:`numpy.dtype` of any, each, and every value. + dtype: numpy.dtype + + #: The number of arrays for which the storage contains values. + nb_arrays: int + + #: The number of entities in the current Simulation. + nb_cells_by_array: int + + #: The number of times the Variable has been computed. + nb_requests: int + + #: Average times a stored array has been read. + nb_requests_by_array: int + + #: The total number of bytes used by the storage. + total_nb_bytes: int diff --git a/pyproyect.toml b/pyproyect.toml new file mode 100644 index 0000000000..1e2a43ee4e --- /dev/null +++ b/pyproyect.toml @@ -0,0 +1,2 @@ +[tool.black] +target-version = ["py39", "py310", "py311"]