88"""
99
1010from unyt import unyt_quantity , unyt_array
11- from numpy import tanh , log10
11+ from numpy import tanh , log10 , logical_and
1212from matplotlib .pyplot import Axes
1313from matplotlib import rcParams
1414
1717from astropy .cosmology import wCDM , FlatLambdaCDM
1818
1919import h5py
20- import json
2120
2221from typing import Union , Optional , List
2322
@@ -131,7 +130,7 @@ class ObservationalData(object):
131130 Attributes
132131 ----------
133132 name: str
134- Name of the observation for users to identifty
133+ Name of the observation for users to identify
135134
136135 x_units: unyt_quantity
137136 Units for horizontal axes
@@ -155,6 +154,16 @@ class ObservationalData(object):
155154 unyt_array of shape 1XN (symmetric) or 2XN (non-symmetric)
156155 such that it can be passed to plt.errorbar easily.
157156
157+ lolims: Union[unyt_array[bool], None]
158+ A bool unyt_array specifying whether y data values are only lower limits
159+ (with entries equal to True or False for each data point, with `True' standing
160+ for the lower limits). The default is None, i.e. all values are not lower limits.
161+
162+ uplims: Union[unyt_array[bool], None]
163+ A bool unyt_array specifying whether y data values are only upper limits
164+ (with entries equal to True or False for each data point, with `True' standing
165+ for the upper limits). The default is None, i.e. all values are not upper limits.
166+
158167 x_comoving: bool
159168 Whether or not the horizontal values are comoving (True)
160169 or physical (False)
@@ -220,6 +229,9 @@ class ObservationalData(object):
220229 # scatter
221230 x_scatter : Union [unyt_array , None ]
222231 y_scatter : Union [unyt_array , None ]
232+ # are y data points upper/lower limits?
233+ lower_limits : Union [unyt_array , None ]
234+ upper_limits : Union [unyt_array , None ]
223235 # x and y are comoving?
224236 x_comoving : bool
225237 y_comoving : bool
@@ -303,6 +315,20 @@ def load(self, filename: str, prefix: Optional[str] = None):
303315 except KeyError :
304316 self .y_scatter = None
305317
318+ try :
319+ self .lower_limits = unyt_array .from_hdf5 (
320+ filename , dataset_name = f"{ prefix } lower_limits" , group_name = "y"
321+ )
322+ except KeyError :
323+ self .lower_limits = None
324+
325+ try :
326+ self .upper_limits = unyt_array .from_hdf5 (
327+ filename , dataset_name = f"{ prefix } upper_limits" , group_name = "y"
328+ )
329+ except KeyError :
330+ self .upper_limits = None
331+
306332 with h5py .File (filename , "r" ) as handle :
307333 metadata = handle [f"{ prefix } metadata" ].attrs
308334
@@ -367,6 +393,16 @@ def write(self, filename: str, prefix: Optional[str] = None):
367393 filename , dataset_name = f"{ prefix } scatter" , group_name = "y"
368394 )
369395
396+ if self .lower_limits is not None :
397+ self .lower_limits .write_hdf5 (
398+ filename , dataset_name = f"{ prefix } lower_limits" , group_name = "y"
399+ )
400+
401+ if self .upper_limits is not None :
402+ self .upper_limits .write_hdf5 (
403+ filename , dataset_name = f"{ prefix } upper_limits" , group_name = "y"
404+ )
405+
370406 with h5py .File (filename , "a" ) as handle :
371407 metadata = handle .create_group (f"{ prefix } metadata" ).attrs
372408
@@ -434,6 +470,8 @@ def associate_y(
434470 scatter : Union [unyt_array , None ],
435471 comoving : bool ,
436472 description : str ,
473+ lolims : Union [unyt_array , None ] = None ,
474+ uplims : Union [unyt_array , None ] = None ,
437475 ):
438476 """
439477 Associate an y quantity with this observational data instance.
@@ -453,15 +491,53 @@ def associate_y(
453491
454492 description: str
455493 Short description of the data, e.g. Stellar Masses
494+
495+ lolims: Union[unyt_array, None]
496+ A bool unyt_array indicating whether the y values are lower limits.
497+ The default is None, meaning no data point is a lower limit.
498+
499+ uplims: Union[unyt_array, None]
500+ A bool unyt_array indicating whether the y values are upper limits.
501+ The default is None, meaning no data point is an upper limit.
456502 """
457503
458504 self .y = array
459505 self .y_units = array .units
460506 self .y_comoving = comoving
461507 self .y_description = description
462508
509+ if lolims is not None :
510+ self .lower_limits = lolims
511+
512+ # Check for invalid input
513+ if uplims is not None :
514+ if sum (logical_and (lolims , uplims )):
515+ raise RuntimeError (
516+ "Entries of the unyt arrays representing lower and upper limits must be "
517+ "of 'bool' type and cannot both be 'True' for the same data points."
518+ )
519+
520+ else :
521+ self .lower_limits = None
522+
523+ if uplims is not None :
524+ self .upper_limits = uplims
525+ else :
526+ self .upper_limits = None
527+
463528 if scatter is not None :
464529 self .y_scatter = scatter .to (self .y_units )
530+ # In the absence of provided scatter values, set default values to indicate the lower or upper limits
531+ elif lolims is not None or uplims is not None :
532+ self .y_scatter = self .y * 0.0
533+ if lolims is not None :
534+ self .y_scatter [self .lower_limits .value ] = (
535+ self .y [self .lower_limits .value ] / 3.0
536+ )
537+ if uplims is not None :
538+ self .y_scatter [self .upper_limits .value ] = (
539+ self .y [self .upper_limits .value ] / 3.0
540+ )
465541 else :
466542 self .y_scatter = None
467543
@@ -653,6 +729,12 @@ def plot_on_axes(self, axes: Axes, errorbar_kwargs: Union[dict, None] = None):
653729 self .y ,
654730 yerr = self .y_scatter ,
655731 xerr = self .x_scatter ,
732+ lolims = self .lower_limits .value
733+ if self .lower_limits is not None
734+ else None ,
735+ uplims = self .upper_limits .value
736+ if self .upper_limits is not None
737+ else None ,
656738 ** kwargs ,
657739 label = data_label ,
658740 )
0 commit comments