Coverage for src/python/dandeliion/client/solution.py: 98%
56 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-24 10:44 +0100
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-24 10:44 +0100
1"""
2@file python/dandeliion/client/solution.py
4Module containing class for handling access to solutions
5"""
7#
8# Copyright (C) 2024-2025 Dandeliion Team
9#
10# This library is free software; you can redistribute it and/or modify it under
11# the terms of the GNU Lesser General Public License as published by the Free
12# Software Foundation; either version 3.0 of the License, or (at your option)
13# any later version.
14#
15# This library is distributed in the hope that it will be useful, but WITHOUT
16# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
18# details.
19#
20# You should have received a copy of the GNU Lesser General Public License
21# along with this library; if not, write to the Free Software Foundation, Inc.,
22# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23#
25# built-in modules
26import logging
27import copy
28from typing import Protocol, Optional
29from collections.abc import Mapping
31# custom modules
32from .exceptions import DandeliionAPIException
34# third-party modules
35import numpy as np
36import numpy.typing
39logger = logging.getLogger(__name__)
42class Simulator(Protocol):
43 """ Simulator Protocol """
44 def update_results(self, prefetched_data: dict, keys: list = None, inline: bool = False) -> Optional[dict]: ...
45 def get_status(self, prefetched_data: dict) -> str: ...
48class InterpolatedArray(np.ndarray):
49 """
50 Subclass of ndarray providing function call for linear interpolation
51 """
52 def __new__(cls, t: np.typing.ArrayLike, y: np.typing.ArrayLike, **kwargs):
53 instance = np.asarray(y, **kwargs).view(cls)
54 instance.t = np.array(t)
55 return instance
57 def __call__(self, t):
58 """
59 function call to return interpolated value
60 """
61 # check that 1-d array
62 if not (len(self.t.shape) == len(self.shape) == 1):
63 raise ValueError('x and y must be 1-d array-like objects')
64 # check that array of same length
65 if self.t.shape != self.shape:
66 raise ValueError('x and y must be of same length')
67 return np.interp(t, self.t, self)
70class Solution(Mapping):
71 """Dictionary-style class for the solutions of a simulation run
72 returned by :meth:`solve`
73 """
75 _data: dict = None
76 _sim: Simulator = None
78 def __init__(self, sim: Simulator, prefetched_data: dict, time_column: str = None):
79 """
80 Constructor
82 Args:
83 sim (Simulator): simulator instance for fetching data from server
84 prefetched_data (dict): existing (meta) data
85 time_column (str): label of time column (used for interpolation)
86 """
87 self._sim = sim
88 self._data = prefetched_data
89 self._time_column = time_column
91 def _init_solution(self):
92 """
93 inits prefetched solution from simulator if necessary
94 """
95 logger.debug('Initialising solutions')
96 self._sim.update_results(self._data, inline=True)
97 # if solution still not initialised (e.g. because simulation
98 # failed or has not finished yet), raise Exception
99 if self._data['Solution'] is None:
100 raise DandeliionAPIException(
101 'Solution not ready (yet). Check status for details.'
102 )
104 def __getitem__(self, key: str):
105 """Returns the results requested by the key.
107 Args:
108 key (str): key for results to be returned.
110 Returns:
111 object: data as requested by provided key
112 """
114 # if solution not initialised yet, try to fetch from server
115 if self._data.get('Solution', None) is None:
116 self._init_solution()
118 if key not in self._data['Solution']:
119 raise KeyError(
120 f'Column for {key} does not exist in solution.'
121 )
122 # fetch data if necessary
123 if self._data['Solution'][key] is None:
124 logger.info(f"Fetching '{key}' column from simulator")
125 self._sim.update_results(self._data, keys=[key], inline=True)
126 if self._time_column:
127 return InterpolatedArray(t=self._data['Solution'][self._time_column], y=self._data['Solution'][key])
128 else:
129 return copy.deepcopy(self._data['Solution'][key])
131 def __len__(self):
132 """Returns the number of fields in the solutions.
134 Returns:
135 int: number of fields
136 """
137 if self._data.get('Solution', None) is None:
138 self._init_solution()
139 return len(self._data['Solution'])
141 def __iter__(self):
142 """Returns an iterator on the solutions fields.
144 Returns:
145 iterator
146 """
147 if self._data.get('Solution', None) is None:
148 self._init_solution()
149 yield from self._data['Solution']
151 @property
152 def status(self):
153 """Returns the status of the simulation run linked to this solutions
155 Returns:
156 str: current status of simulation run ('queued', 'running', 'failed', 'success')
157 """
158 return self._sim.get_status(self._data)