|
2 | 2 |
|
3 | 3 | from __future__ import annotations
|
4 | 4 |
|
5 |
| -import re |
6 | 5 | from collections.abc import Sequence
|
7 | 6 | from enum import Enum
|
8 | 7 | from itertools import chain
|
9 | 8 | from pathlib import Path
|
10 |
| -from typing import Annotated, Literal |
| 9 | +from typing import Annotated |
11 | 10 |
|
12 | 11 | import numpy as np
|
13 | 12 | import pandas as pd
|
@@ -192,6 +191,14 @@ class Observable(BaseModel):
|
192 | 191 | noise_distribution: NoiseDistribution = Field(
|
193 | 192 | alias=C.NOISE_DISTRIBUTION, default=NoiseDistribution.NORMAL
|
194 | 193 | )
|
| 194 | + #: Placeholder symbols for the observable formula. |
| 195 | + observable_placeholders: list[sp.Symbol] = Field( |
| 196 | + alias=C.OBSERVABLE_PLACEHOLDERS, default=[] |
| 197 | + ) |
| 198 | + #: Placeholder symbols for the noise formula. |
| 199 | + noise_placeholders: list[sp.Symbol] = Field( |
| 200 | + alias=C.NOISE_PLACEHOLDERS, default=[] |
| 201 | + ) |
195 | 202 |
|
196 | 203 | #: :meta private:
|
197 | 204 | model_config = ConfigDict(
|
@@ -221,37 +228,24 @@ def _sympify(cls, v):
|
221 | 228 |
|
222 | 229 | return sympify_petab(v)
|
223 | 230 |
|
224 |
| - def _placeholders( |
225 |
| - self, type_: Literal["observable", "noise"] |
226 |
| - ) -> set[sp.Symbol]: |
227 |
| - formula = ( |
228 |
| - self.formula |
229 |
| - if type_ == "observable" |
230 |
| - else self.noise_formula |
231 |
| - if type_ == "noise" |
232 |
| - else None |
233 |
| - ) |
234 |
| - if formula is None or formula.is_number: |
235 |
| - return set() |
236 |
| - |
237 |
| - if not (free_syms := formula.free_symbols): |
238 |
| - return set() |
| 231 | + @field_validator( |
| 232 | + "observable_placeholders", "noise_placeholders", mode="before" |
| 233 | + ) |
| 234 | + @classmethod |
| 235 | + def _sympify_id_list(cls, v): |
| 236 | + if v is None: |
| 237 | + return [] |
239 | 238 |
|
240 |
| - # TODO: add field validator to check for 1-based consecutive numbering |
241 |
| - t = f"{re.escape(type_)}Parameter" |
242 |
| - o = re.escape(self.id) |
243 |
| - pattern = re.compile(rf"(?:^|\W)({t}\d+_{o})(?=\W|$)") |
244 |
| - return {s for s in free_syms if pattern.match(str(s))} |
| 239 | + if isinstance(v, float) and np.isnan(v): |
| 240 | + return [] |
245 | 241 |
|
246 |
| - @property |
247 |
| - def observable_placeholders(self) -> set[sp.Symbol]: |
248 |
| - """Placeholder symbols for the observable formula.""" |
249 |
| - return self._placeholders("observable") |
| 242 | + if isinstance(v, str): |
| 243 | + v = v.split(C.PARAMETER_SEPARATOR) |
| 244 | + elif not isinstance(v, Sequence): |
| 245 | + v = [v] |
250 | 246 |
|
251 |
| - @property |
252 |
| - def noise_placeholders(self) -> set[sp.Symbol]: |
253 |
| - """Placeholder symbols for the noise formula.""" |
254 |
| - return self._placeholders("noise") |
| 247 | + v = [pid.strip() for pid in v] |
| 248 | + return [sympify_petab(_valid_petab_id(pid)) for pid in v if pid] |
255 | 249 |
|
256 | 250 |
|
257 | 251 | class ObservableTable(BaseModel):
|
@@ -289,6 +283,12 @@ def to_df(self) -> pd.DataFrame:
|
289 | 283 | noise = record[C.NOISE_FORMULA]
|
290 | 284 | record[C.OBSERVABLE_FORMULA] = petab_math_str(obs)
|
291 | 285 | record[C.NOISE_FORMULA] = petab_math_str(noise)
|
| 286 | + record[C.OBSERVABLE_PLACEHOLDERS] = C.PARAMETER_SEPARATOR.join( |
| 287 | + map(str, record[C.OBSERVABLE_PLACEHOLDERS]) |
| 288 | + ) |
| 289 | + record[C.NOISE_PLACEHOLDERS] = C.PARAMETER_SEPARATOR.join( |
| 290 | + map(str, record[C.NOISE_PLACEHOLDERS]) |
| 291 | + ) |
292 | 292 | return pd.DataFrame(records).set_index([C.OBSERVABLE_ID])
|
293 | 293 |
|
294 | 294 | @classmethod
|
|
0 commit comments