diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c7e34264..37f36815 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,7 +44,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: wheelhouse + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl build_source: @@ -69,8 +69,8 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: sdist - path: ./dist/*.tar.gz + name: cibw-sdist + path: dist/*.tar.gz publish: name: Publish to PyPI @@ -81,17 +81,25 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Download sdist artifacts to dist/ - uses: actions/download-artifact@v4.1.7 + - name: Download sdist and wheelhouse artifacts to dist/ + uses: actions/download-artifact@v4 with: - name: sdist - path: dist/ - - - name: Download wheelhouse artifacts to dist/ - uses: actions/download-artifact@v4.1.7 - with: - name: wheelhouse - path: dist/ + # unpacks all CIBW artifacts into dist/ + pattern: cibw-* + path: dist + merge-multiple: true + +# - name: Download sdist artifacts to dist/ +# uses: actions/download-artifact@v4 +# with: +# name: sdist +# path: dist/ + +# - name: Download wheelhouse artifacts to dist/ +# uses: actions/download-artifact@v4 +# with: +# name: wheelhouse +# path: dist/ - name: Publish package to PyPI # All files in dist/ are published diff --git a/documents/00_MMv3_changes.md b/documents/00_MMv3_changes.md index 87d3b037..c109d4a8 100644 --- a/documents/00_MMv3_changes.md +++ b/documents/00_MMv3_changes.md @@ -1,19 +1,15 @@ # MulensModel V3 There were two major goals for MulensModel V3: -- Allow for N sources (However, xallarap can only be used with binary source -models) -- Allow the user direct access to derivatives relative to the point lens model -(in addition to derivatives relative to chi2) +- Allow for N sources (However, xallarap can only be used with binary source models) +- Allow the user a direct access to partial derivatives of the magnification relative to the point lens model parameters (in addition to chi2 derivatives) Implementing these changes involved some major updates to the internal architecture, but these should mostly be invisible to the user (although they add new functionality). -We have also cleaned up some problems/bugs/features that will impact the user -experience: -- Shifted the convention of `alpha` by 180 deg to make MM compatible with -convention of Skowron et al. 2011. +We have also cleaned up some problems/bugs/features that will impact the user experience: +- Shifted the convention of `alpha` by 180 deg to make MM compatible with convention of Skowron et al. 2011. - Removing `astropy` units from microlensing parameters. - Generally improving consistency in naming conventions. - Deprecated functions and properties removed. @@ -35,21 +31,16 @@ Use `fix_blend_flux` instead. ### Model Class -- ADDED `get_magnification_curve()` and `get_magnification_curves()` -(for multi-source models). -- `get_lc()`: updated documentation to reflect that this returns MAGNITUDES -(not magnifications) +- ADDED `get_magnification_curve()` and `get_magnification_curves()` (for multi-source models). +- `get_lc()`: updated documentation to reflect that this returns MAGNITUDES (not magnifications) - `get_magnification()`: - REMOVED keyword `flux_ratio_constraint` - - default value of `separate` changed to None (which defaults to `True` if -`source_flux_ratio` is provided and to `False` otherwise). + - default value of `separate` changed to None (which defaults to `True` if `source_flux_ratio` is provided and to `False` otherwise). - REMOVED deprecated functions and attributes: - `plot_magnification()`: REMOVED keyword `flux_ratio_constraint`. - - `plot_lc()`: REMOVED keywords `data_ref`, `flux_ratio_constraint`, -`fit_blending`, `f_source`, and `f_blend`. + - `plot_lc()`: REMOVED keywords `data_ref`, `flux_ratio_constraint`, `fit_blending`, `f_source`, and `f_blend`. - `plot_trajectory()`: REMOVED keyword `show_data`. - - `set_default_magnification_method()`. Use -`Model.default_magnification_method = XXX`, instead. + - `set_default_magnification_method()`. Use `Model.default_magnification_method = XXX`, instead. - `magnification()` replaced by `get_magnification()`. - `reset_plot_properties()` @@ -60,21 +51,14 @@ immediately converted into a `ModelParameters` object, so many of these changes could affect the user experience, even if they had not been interacting directly with the `ModelParameters` class. -- `Astropy` units REMOVED (i.e., now everything is a `float`) from properties: -`t_star`, `t_eff`, `t_E`, `alpha`, `ds_dt`, `dalpha_dt`, `t_star_1`, `t_star_2`, -`gamma_parallel`, `gamma_perp`, and `gamma`. Also REMOVED from function -`get_alpha()`. +- `Astropy` units REMOVED (i.e., now everything is a `float`) from properties: `t_star`, `t_eff`, `t_E`, `alpha`, `ds_dt`, `dalpha_dt`, `t_star_1`, `t_star_2`, `gamma_parallel`, `gamma_perp`, and `gamma`. Also REMOVED from function `get_alpha()`. - Property `pi_E` REMOVED. Properties `pi_E_E` and `pi_E_N` are still there. -- Properties that are not defined now raise `AttributeError` (previously `rho` -was returning None, and some other ones raised `KeyError`). -- Warnings for unexpected values of angles (e.g., 1000 deg) are not raised -anymore. +- Properties that are not defined now raise `AttributeError` (previously `rho` was returning None, and some other ones raised `KeyError`). +- Warnings for unexpected values of angles (e.g., 1000 deg) are not raised anymore. - Changes in default values for `t_0_par` and `t_0_kep`: - - If `t_0_par` is not defined, then it defaults to `t_0_kep`, `t_0`, `t_0_1` -in that order. - - If `t_0_kep` is not defined, then it defaults to `t_0_par`, `t_0`, `t_0_1` -in that order. + - If `t_0_par` is not defined, then it defaults to `t_0_kep`, `t_0`, `t_0_1` in that order. + - If `t_0_kep` is not defined, then it defaults to `t_0_par`, `t_0`, `t_0_1` in that order. Also, REMOVED `which_parameters()` from modelparameters.py because it not very useful and it was very complicated. @@ -99,8 +83,7 @@ instead of the `times` argument. - ADDED: - `get_d_A_d_rho()` - `magnification_curve` and `magnification_curves` -- DEPRECATED subclass `FSPL_Derivatives`. Most its methods are replaced by -`PointLens` class(es) +- DEPRECATED subclass `FSPL_Derivatives`. Most its methods are replaced by `PointLens` class(es) - DEPRECATED: - `get_d_A_d_u_for_PSPL_model()` @@ -118,8 +101,7 @@ could be accessed instead through `FitData.magnification_curve.XXX()`. The PointLens and BinaryLens classes were subdivided into separate classes for each magnification method. -- Function `get_pspl_magnification()` from `pointlens.py` was removed. Use -classes listed in next point instead. +- Function `get_pspl_magnification()` from `pointlens.py` was removed. Use classes listed in next point instead. - Point Lens classes ADDED: - `PointSourcePointLensMagnification` - `FiniteSourceUniformGould94Magnification` @@ -150,4 +132,4 @@ classes listed in next point instead. - ADDED `EllipUtils`. - `MagnificationCurve`: - ADDED methods `get_d_A_d_params()` and `get_d_A_d_rho()`. - - ADDED property `methods_indices`. \ No newline at end of file + - ADDED property `methods_indices`. diff --git a/documents/MM_v3.md b/documents/MM_v4.md similarity index 65% rename from documents/MM_v3.md rename to documents/MM_v4.md index 302cb952..e9b0d4f3 100644 --- a/documents/MM_v3.md +++ b/documents/MM_v4.md @@ -1,16 +1,12 @@ -# What we want to change when going from v2.X.Y to v3.0.0? +# What we want to change when going from v3.X.Y to v4.0.0? Once the changes are accepted to be made, **mark them in the code using warnings.warn("XXX", FutureWarning)** and note it here. Also release a version that differs from previous one only in these warnings - this will allow users to correct their codes. Also give **suggested changes in warnings**. ### Major changes: - * search for all "deprecated" are remove it - * rename Caustics -> CausticsBinary and CausticsWithShear -> CausticsBinaryWithShear (and files) so that they're consistent with CausticsPointWithShear - ??? ### Minor changes: - * Delete ModelParameters.pi\_E and leave pi\_E\_N and pi\_E\_E - it is not really used and just complicates the code inside * Remove ModelParameters.as\_dict() because it is the same as ModelParameters.parameters * `ModelParameters.is_static` -> `is_lens_static` * ephemerides\_file -> ephemeris\_file - maybe @@ -18,16 +14,10 @@ Once the changes are accepted to be made, **mark them in the code using warnings * test\_MulensData.py - in test\_copy() remove warnings.catch\_warnings() because you remove coords, ra, and dec from init ### Yet unsorted/undecided: - * shift alpha by 180 deg if large update is made to follow Skowron et al. (2011) convention * remove MulensData.bad - see https://github.com/rpoleski/MulensModel/issues/40 * `Model.set\_times()` - `n\_epochs` should be None as default, so that we can check if both dt and `n\_epochs` were set * Caustics.get\_caustics() should return np.arrays, not lists * check all NotImplementedError and maybe remove some functions/options - * somehow change which\_parameters() in modelparameters.py - maybe remove * new class for a collection of datasets to make looping over datasets easier; also there will be data\_ref defined - * the same order of arguments in plotting functions (if possible) - * ModelParameters - all parameters should be float, not astropy.Quantity objects * see (this comment by Jen)[https://github.com/rpoleski/MulensModel/pull/15#issuecomment-1080879537] on how magnification methods are named and called in different parts of the code - -### Version 4: - * Add an Observatory class. + * Add an Observatory class - for terresital parallax diff --git a/examples/example_16/ob03235_2_full.yaml b/examples/example_16/ob03235_2_full.yaml index 4fd95e88..b6471c06 100644 --- a/examples/example_16/ob03235_2_full.yaml +++ b/examples/example_16/ob03235_2_full.yaml @@ -120,6 +120,7 @@ plots: best model: # You can skip the line below - the light curve will be plotted on screen. file: ob03235_2_model.png + # In case you want an interactive plot made using plotly: interactive : ob03235_2_model.html time range: 2452820 2452855 magnitude range: 19.3 16.9 diff --git a/examples/example_16/ulens_model_fit.py b/examples/example_16/ulens_model_fit.py index 85af3521..c0e4a848 100644 --- a/examples/example_16/ulens_model_fit.py +++ b/examples/example_16/ulens_model_fit.py @@ -47,7 +47,7 @@ raise ImportError('\nYou have to install MulensModel first!\n') -__version__ = '0.41.0' +__version__ = '0.42.0' class UlensModelFit(object): @@ -879,10 +879,6 @@ def _check_plots_parameters_best_model_interactive(self): """ Check if there is no problem with interactive best plot """ - if "second Y scale" in self._plots['best model']: - msg = "Interactive plot will not have a second Y scale. This feature is not yet implemented." - raise NotImplementedError(msg) - def _check_plots_parameters_best_model_Y_scale(self): """ Check if parameters for second Y scale make sense. @@ -3695,21 +3691,16 @@ def _mark_second_Y_axis_in_best_plot(self): Mark the second (right-hand side) scale for Y axis in the best model plot """ - (magnifications, labels, ylim, ax2) = self._second_Y_axis_settings() - (A_range, ref_fluxes) = self._second_Y_axis_get_fluxes(ylim) - out1, out2 = False, False - if magnifications == "optimal": - (magnifications, labels, out1) = self._second_Y_axis_optimal( - ax2, *A_range) - self._second_Y_axis_minor_ticks(ax2, magnifications, ref_fluxes) - flux = ref_fluxes[0] * np.array(magnifications) + ref_fluxes[1] - out2 = self._second_Y_axis_warnings(flux, labels, magnifications, - *A_range) - if out1 or out2: + (warning1, warning2, label, ylim, color, ax2, labels, ticks, minor_ticks) = self._get_second_Y_axis_settings() + if warning1 or warning2: ax2.get_yaxis().set_visible(False) - return + return + if minor_ticks is not None: + ax2.set_yticks(minor_ticks, minor=True) + ax2.set_ylabel(label).set_color(color) + ax2.spines['right'].set_color(color) + ax2.tick_params(axis='y', direction="in", which="both", colors=color) - ticks = mm.Utils.get_mag_from_flux(flux) try: # matplotlib version 3.5 or later ax2.set_yticks(ticks=ticks, labels=labels) except Exception: # matplotlib version 3.4.X or smaller @@ -3718,23 +3709,36 @@ def _mark_second_Y_axis_in_best_plot(self): ax2.set_ylim(ylim[0], ylim[1]) + def _get_second_Y_axis_settings(self, ylim=False): + """Creats settings for the second Y axis for the best model plot + """ + if not ylim: + ylim = plt.ylim() + (magnifications, color, label, labels, ax2) = self._second_Y_axis_settings() + (A_range, ref_fluxes) = self._second_Y_axis_get_fluxes(ylim) + warning1, warning2 = False, False + minor_ticks = None + if magnifications == "optimal": + (magnifications, labels, warning1) = self._second_Y_axis_optimal( + ax2, *A_range) + minor_ticks = self._second_Y_axis_minor_ticks(ax2, magnifications, ref_fluxes) + flux = ref_fluxes[0] * np.array(magnifications) + ref_fluxes[1] + warning2 = self._second_Y_axis_warnings(flux, labels, magnifications, *A_range) + ticks = mm.Utils.get_mag_from_flux(flux) + + return (warning1, warning2, label, ylim, color, ax2, labels, ticks, minor_ticks) + def _second_Y_axis_settings(self): """ - Get and apply settings for the second Y axis + Get settings for the second Y axis """ settings = self._plots['best model']["second Y scale"] magnifications = settings['magnifications'] color = settings.get("color", "black") label = settings.get("label", "Magnification") labels = settings.get("labels") - ylim = plt.ylim() - ax2 = plt.gca().twinx() - ax2.set_ylabel(label).set_color(color) - ax2.spines['right'].set_color(color) - ax2.tick_params(axis='y', direction="in", which="both", colors=color) - - return (magnifications, labels, ylim, ax2) + return (magnifications, color, label, labels, ax2) def _second_Y_axis_get_fluxes(self, ylim): """ @@ -3783,10 +3787,9 @@ def _second_Y_axis_minor_ticks(self, ax2, A_values, ref_fluxes): ax2.minorticks_on() minor_ticks_A = np.array(ax2.yaxis.get_ticklocs(minor=True)) minor_ticks_A = minor_ticks_A[~np.isin(minor_ticks_A, A_values)] - minor_ticks_flux = ref_fluxes[0] * minor_ticks_A + ref_fluxes[1] minor_ticks_mag = mm.Utils.get_mag_from_flux(minor_ticks_flux) - ax2.set_yticks(minor_ticks_mag, minor=True) + return minor_ticks_mag def _second_Y_axis_warnings(self, flux, labels, A_values, A_min, A_max): """ @@ -3885,6 +3888,9 @@ def _prepare_interactive_layout(self, scale, kwargs_all, ylim, ylim_residuals): **kwargs_model, **kwargs_interactive ) + if "second Y scale" in self._plots['best model']: + layout = self._add_second_Y_axis_to_interactive_layout(layout, ylim) + layout = go.Layout(layout) return layout, kwargs_model, kwargs_interactive, kwargs def _get_kwargs_for_plotly_plot(self, scale): @@ -3931,10 +3937,10 @@ def _make_interactive_layout(self, ylim, ylim_residuals, height_ratios, hspace, tickwidth=sizes[6], linewidth=sizes[6], linecolor=colors[0], tickfont=font_base) kwargs_y = {'mirror': 'all', **kwargs_} kwargs_x = {'range': [t_start, t_stop], **kwargs_} - layout = go.Layout( + layout = dict( autosize=True, width=width, height=height, showlegend=True, - legend=dict( - x=1.02, y=.98, bgcolor=paper_bgcolor, bordercolor=colors[2], borderwidth=sizes[6], font=font_legend), + legend=dict(x=0, y=-0.2, yanchor='top', bgcolor=paper_bgcolor, bordercolor=colors[2], + borderwidth=sizes[6], font=font_legend), paper_bgcolor=paper_bgcolor, plot_bgcolor=paper_bgcolor, font=font_base, yaxis=dict(title_text='Magnitude', domain=[hsplit+(hspace/2), 1], range=ylim, **kwargs_y), yaxis2=dict(title_text='Residuals', domain=[0, hsplit-(hspace/2)], anchor="x", range=ylim_residuals, @@ -3944,6 +3950,28 @@ def _make_interactive_layout(self, ylim, ylim_residuals, height_ratios, hspace, xaxis2=dict(title_text=xtitle, anchor="y2", mirror='all', scaleanchor='x', matches='x', **kwargs_x) ) return layout + + def _add_second_Y_axis_to_interactive_layout(self, layout, ylim): + """Modifiing plotly.graph_objects.Layout object by adding second Y axis + """ + layout['yaxis']['mirror'] = False + layout['yaxis3'] = layout['yaxis'].copy() + layout['yaxis3'].update(dict(overlaying="y", side="right", scaleanchor="y")) + settings = self._get_interactive_second_Y_axis_settings(ylim) + layout['yaxis3'].update(settings) + return layout + + def _get_interactive_second_Y_axis_settings(self, ylim): + """Creats dictionary with settings for the second Y axis for the interactive plot + """ + (warning1, warning2, label, ylim, color, ax2, labels, ticks, + minor_ticks) = self._get_second_Y_axis_settings(ylim) + if warning1 or warning2: + return {} + if minor_ticks is not None: + minor_ticks = dict(ticks='inside', tickmode='array', tickvals=minor_ticks) + return dict(title_text=label, linecolor=color, tickcolor=color, tickmode='array', tickvals=ticks, + ticktext=labels, minor=minor_ticks) def _get_time_span_data(self): """ @@ -3965,7 +3993,8 @@ def _make_interactive_lc_traces(self, f_source_0, f_blend_0, sizes, colors, opac """ traces_lc = [] subtract = mm.utils.PlotUtils.find_subtract(subtract_2450000, subtract_2460000) - times = np.linspace(t_start, t_stop, num=5000) - subtract + time_grid = int(t_stop-t_start)*7 + times = np.linspace(t_start, t_stop, num=time_grid) if isinstance(name, type(None)): showlegend = False @@ -3976,6 +4005,7 @@ def _make_interactive_lc_traces(self, f_source_0, f_blend_0, sizes, colors, opac if dataset.ephemerides_file is None: lc = self._model.get_lc( times=times, source_flux=f_source_0, blend_flux=f_blend_0, gamma=gamma, bandpass=bandpass) + times = times - subtract traces_lc.append(self._make_interactive_scatter_lc( times, lc, name, showlegend, colors[1], sizes[1], dash)) break @@ -4049,7 +4079,13 @@ def _add_interactive_data_traces(self, kwargs_interactive, phot_fmt='mag', data_ dataset_index, times, y_value, y_err, xaxis='x', yaxis='y', showlegend=True, show_errorbars=show_errorbars, show_bad=show_bad, **kwargs_interactive) traces_data.extend(trace_data) - + if "second Y scale" in self._plots['best model'] and dataset_index == 0: + kwargs_interactive_secY = kwargs_interactive.copy() + kwargs_interactive_secY['opacity'] = 0. + trace_data = self._make_one_interactive_data_trace( + dataset_index, times, y_value, y_err, xaxis='x', yaxis='y3', showlegend=False, + show_errorbars=show_errorbars, show_bad=show_bad, **kwargs_interactive_secY) + traces_data.extend(trace_data) for trace in traces_data: self._interactive_fig.add_trace(trace) @@ -4093,10 +4129,11 @@ def _make_interactive_good_data_trace( def _make_interactive_data_trace(self, x, y, y_err, dataset, opacity, sizes, xaxis, yaxis, showlegend, show_errorbars, color_override=None, error_visible=True): """Creates single plotly.graph_objects.Scatter object for good or bad data.""" + label = dataset.plot_properties['label'] color = color_override if color_override else dataset.plot_properties['color'] error_y = dict(type='data', array=y_err, visible=error_visible, thickness=sizes[2], width=sizes[3]) marker = dict(color=color, size=sizes[0], line=dict(color=color, width=1)) - return go.Scatter(x=x, y=y, opacity=opacity, name=dataset.plot_properties['label'], mode='markers', + return go.Scatter(x=x, y=y, opacity=opacity, name=label, legendgroup=label, mode='markers', showlegend=showlegend, error_y=error_y, marker=marker, xaxis=xaxis, yaxis=yaxis) def _make_interactive_bad_data_trace(self, dataset, times, y_value, y_err, opacity, sizes,