From 3fa22846f95c93d45005cab3be91b15542ec1dc3 Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 13:51:22 -0500 Subject: [PATCH 01/12] begin refactor, move widgets --- rivers2stratigraphy/__init__.py | 5 +- rivers2stratigraphy/channel.py | 2 +- rivers2stratigraphy/main.py | 3 +- rivers2stratigraphy/utils.py | 330 ------------------------------- rivers2stratigraphy/widgets.py | 335 ++++++++++++++++++++++++++++++++ 5 files changed, 340 insertions(+), 335 deletions(-) create mode 100644 rivers2stratigraphy/widgets.py diff --git a/rivers2stratigraphy/__init__.py b/rivers2stratigraphy/__init__.py index 5bd0be3..24db764 100644 --- a/rivers2stratigraphy/__init__.py +++ b/rivers2stratigraphy/__init__.py @@ -1,3 +1,2 @@ -from . import launcher - -launcher.import_runner() \ No newline at end of file +# from . import launcher +# launcher.import_runner() \ No newline at end of file diff --git a/rivers2stratigraphy/channel.py b/rivers2stratigraphy/channel.py index d62e407..f6d970e 100644 --- a/rivers2stratigraphy/channel.py +++ b/rivers2stratigraphy/channel.py @@ -5,7 +5,7 @@ import shapely.geometry as sg import shapely.ops as so -from . import geom, sedtrans, utils +import geom, sedtrans, utils class ActiveChannel(object): diff --git a/rivers2stratigraphy/main.py b/rivers2stratigraphy/main.py index 2aa9f81..e5bc8f2 100644 --- a/rivers2stratigraphy/main.py +++ b/rivers2stratigraphy/main.py @@ -31,8 +31,9 @@ import sys import gc +# import channel from .channel import ActiveChannel, State, ChannelBody -from . import geom, sedtrans, utils +import geom, sedtrans, utils # model run params diff --git a/rivers2stratigraphy/utils.py b/rivers2stratigraphy/utils.py index 8aaccae..d656d9d 100644 --- a/rivers2stratigraphy/utils.py +++ b/rivers2stratigraphy/utils.py @@ -24,334 +24,4 @@ def normalizeColor(v, minV, maxV): return (v-minV)/(maxV-minV) -class MinMaxSlider(AxesWidget): - """ - A slider representing a floating point range. - Create a slider from *valmin* to *valmax* in axes *ax*. For the slider to - remain responsive you must maintain a reference to it. Call - :meth:`on_changed` to connect to the slider event. - Attributes - ---------- - val : float - Slider value. - """ - def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', - closedmin=True, closedmax=True, slidermin=None, - slidermax=None, dragging=True, valstep=None, **kwargs): - """ - Parameters - ---------- - ax : Axes - The Axes to put the slider in. - label : str - Slider label. - valmin : float - The minimum value of the slider. - valmax : float - The maximum value of the slider. - valinit : float, optional, default: 0.5 - The slider initial position. - valfmt : str, optional, default: "%1.2f" - Used to format the slider value, fprint format string. - closedmin : bool, optional, default: True - Indicate whether the slider interval is closed on the bottom. - closedmax : bool, optional, default: True - Indicate whether the slider interval is closed on the top. - slidermin : Slider, optional, default: None - Do not allow the current slider to have a value less than - the value of the Slider `slidermin`. - slidermax : Slider, optional, default: None - Do not allow the current slider to have a value greater than - the value of the Slider `slidermax`. - dragging : bool, optional, default: True - If True the slider can be dragged by the mouse. - valstep : float, optional, default: None - If given, the slider will snap to multiples of `valstep`. - Notes - ----- - Additional kwargs are passed on to ``self.poly`` which is the - :class:`~matplotlib.patches.Rectangle` that draws the slider - knob. See the :class:`~matplotlib.patches.Rectangle` documentation for - valid property names (e.g., `facecolor`, `edgecolor`, `alpha`). - """ - AxesWidget.__init__(self, ax) - if slidermin is not None and not hasattr(slidermin, 'val'): - raise ValueError("Argument slidermin ({}) has no 'val'" - .format(type(slidermin))) - if slidermax is not None and not hasattr(slidermax, 'val'): - raise ValueError("Argument slidermax ({}) has no 'val'" - .format(type(slidermax))) - self.closedmin = closedmin - self.closedmax = closedmax - self.slidermin = slidermin - self.slidermax = slidermax - self.drag_active = False - self.valmin = valmin - self.valmax = valmax - self.valstep = valstep - valinit = self._value_in_bounds(valinit) - if valinit is None: - valinit = valmin - self.val = valinit - self.valinit = valinit - self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs) - self.vline = ax.axvline(valinit, 0, 1, color='r', lw=1) - - self.valfmt = valfmt - ax.set_yticks([]) - ax.set_xlim((valmin, valmax)) - ax.set_xticks([]) - ax.set_navigate(False) - - self.connect_event('button_press_event', self._update) - self.connect_event('button_release_event', self._update) - if dragging: - self.connect_event('motion_notify_event', self._update) - self.label = ax.text(0.5, -0.5, label, transform=ax.transAxes, - verticalalignment='center', - horizontalalignment='center') - self.mintext = ax.text(-0.02, 0.5, valfmt % valmin, - transform=ax.transAxes, - verticalalignment='center', - horizontalalignment='right') - self.maxtext = ax.text(1.02, 0.5, valfmt % valmax, - transform=ax.transAxes, - verticalalignment='center', - horizontalalignment='left') - self.valtext = ax.text(0.5, 0.45, valfmt % valinit, - transform=ax.transAxes, - verticalalignment='center', - horizontalalignment='center', - weight='bold') - - self.cnt = 0 - self.observers = {} - - self.set_val(valinit) - - def _value_in_bounds(self, val): - """ Makes sure self.val is with given bounds.""" - if self.valstep: - val = np.round((val - self.valmin)/self.valstep)*self.valstep - val += self.valmin - - if val <= self.valmin: - if not self.closedmin: - return - val = self.valmin - elif val >= self.valmax: - if not self.closedmax: - return - val = self.valmax - - if self.slidermin is not None and val <= self.slidermin.val: - if not self.closedmin: - return - val = self.slidermin.val - - if self.slidermax is not None and val >= self.slidermax.val: - if not self.closedmax: - return - val = self.slidermax.val - return val - - def _update(self, event): - """update the slider position""" - if self.ignore(event): - return - - if event.button != 1: - return - - if event.name == 'button_press_event' and event.inaxes == self.ax: - self.drag_active = True - event.canvas.grab_mouse(self.ax) - - if not self.drag_active: - return - - elif ((event.name == 'button_release_event') or - (event.name == 'button_press_event' and - event.inaxes != self.ax)): - self.drag_active = False - event.canvas.release_mouse(self.ax) - return - val = self._value_in_bounds(event.xdata) - if (val is not None) and (val != self.val): - self.set_val(val) - - def set_val(self, val): - """ - Set slider value to *val* - Parameters - ---------- - val : float - """ - xy = self.poly.xy - xy[2] = val, 1 - xy[3] = val, 0 - self.poly.xy = xy - self.valtext.set_text(self.valfmt % val) - if self.drawon: - self.ax.figure.canvas.draw_idle() - # stratcanv = fig.canvas.copy_from_bbox(ax.bbox) - # fig.canvas.restore_region(stratcanv) - self.val = val - if not self.eventson: - return - for cid, func in six.iteritems(self.observers): - func(val) - - def on_changed(self, func): - """ - When the slider value is changed call *func* with the new - slider value - Parameters - ---------- - func : callable - Function to call when slider is changed. - The function must accept a single float as its arguments. - Returns - ------- - cid : int - Connection id (which can be used to disconnect *func*) - """ - cid = self.cnt - self.observers[cid] = func - self.cnt += 1 - return cid - - def disconnect(self, cid): - """ - Remove the observer with connection id *cid* - Parameters - ---------- - cid : int - Connection id of the observer to be removed - """ - try: - del self.observers[cid] - except KeyError: - pass - - def reset(self): - """Reset the slider to the initial value""" - if (self.val != self.valinit): - self.set_val(self.valinit) - - -class NoDrawButton(AxesWidget): - """ - A GUI neutral button. - For the button to remain responsive you must keep a reference to it. - Call :meth:`on_clicked` to connect to the button. - Attributes - ---------- - ax : - The :class:`matplotlib.axes.Axes` the button renders into. - label : - A :class:`matplotlib.text.Text` instance. - color : - The color of the button when not hovering. - hovercolor : - The color of the button when hovering. - """ - - def __init__(self, ax, label, image=None, - color='0.85', hovercolor='0.95'): - """ - Parameters - ---------- - ax : matplotlib.axes.Axes - The :class:`matplotlib.axes.Axes` instance the button - will be placed into. - label : str - The button text. Accepts string. - image : array, mpl image, Pillow Image - The image to place in the button, if not *None*. - Can be any legal arg to imshow (numpy array, - matplotlib Image instance, or Pillow Image). - color : color - The color of the button when not activated - hovercolor : color - The color of the button when the mouse is over it - """ - AxesWidget.__init__(self, ax) - - if image is not None: - ax.imshow(image) - self.label = ax.text(0.5, 0.5, label, - verticalalignment='center', - horizontalalignment='center', - transform=ax.transAxes) - - self.cnt = 0 - self.observers = {} - - self.connect_event('button_press_event', self._click) - self.connect_event('button_release_event', self._release) - # self.connect_event('motion_notify_event', self._motion) - ax.set_navigate(False) - ax.set_facecolor(color) - ax.set_xticks([]) - ax.set_yticks([]) - self.color = color - self.hovercolor = hovercolor - - self._lastcolor = color - - def _click(self, event): - if self.ignore(event): - return - if event.inaxes != self.ax: - return - if not self.eventson: - return - if event.canvas.mouse_grabber != self.ax: - event.canvas.grab_mouse(self.ax) - - def _release(self, event): - if self.ignore(event): - return - if event.canvas.mouse_grabber != self.ax: - return - event.canvas.release_mouse(self.ax) - if not self.eventson: - return - if event.inaxes != self.ax: - return - for cid, func in self.observers.items(): - func(event) - - # def _motion(self, event): - # if self.ignore(event): - # return - # if event.inaxes == self.ax: - # c = self.hovercolor - # return - # else: - # c = self.color - # if c != self._lastcolor: - # self.ax.set_facecolor(c) - # self._lastcolor = c - # if self.drawon: - # self.ax.figure.canvas.draw() - - def on_clicked(self, func): - """ - When the button is clicked, call this *func* with event. - A connection id is returned. It can be used to disconnect - the button from its callback. - """ - cid = self.cnt - self.observers[cid] = func - self.cnt += 1 - return cid - - def disconnect(self, cid): - """remove the observer with connection id *cid*""" - try: - del self.observers[cid] - except KeyError: - pass diff --git a/rivers2stratigraphy/widgets.py b/rivers2stratigraphy/widgets.py new file mode 100644 index 0000000..62398a3 --- /dev/null +++ b/rivers2stratigraphy/widgets.py @@ -0,0 +1,335 @@ +# import numpy as np +from matplotlib.widgets import AxesWidget +# import six + +class MinMaxSlider(AxesWidget): + """ + A slider representing a floating point range. + Create a slider from *valmin* to *valmax* in axes *ax*. For the slider to + remain responsive you must maintain a reference to it. Call + :meth:`on_changed` to connect to the slider event. + Attributes + ---------- + val : float + Slider value. + """ + def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', + closedmin=True, closedmax=True, slidermin=None, + slidermax=None, dragging=True, valstep=None, **kwargs): + """ + Parameters + ---------- + ax : Axes + The Axes to put the slider in. + label : str + Slider label. + valmin : float + The minimum value of the slider. + valmax : float + The maximum value of the slider. + valinit : float, optional, default: 0.5 + The slider initial position. + valfmt : str, optional, default: "%1.2f" + Used to format the slider value, fprint format string. + closedmin : bool, optional, default: True + Indicate whether the slider interval is closed on the bottom. + closedmax : bool, optional, default: True + Indicate whether the slider interval is closed on the top. + slidermin : Slider, optional, default: None + Do not allow the current slider to have a value less than + the value of the Slider `slidermin`. + slidermax : Slider, optional, default: None + Do not allow the current slider to have a value greater than + the value of the Slider `slidermax`. + dragging : bool, optional, default: True + If True the slider can be dragged by the mouse. + valstep : float, optional, default: None + If given, the slider will snap to multiples of `valstep`. + Notes + ----- + Additional kwargs are passed on to ``self.poly`` which is the + :class:`~matplotlib.patches.Rectangle` that draws the slider + knob. See the :class:`~matplotlib.patches.Rectangle` documentation for + valid property names (e.g., `facecolor`, `edgecolor`, `alpha`). + """ + AxesWidget.__init__(self, ax) + + if slidermin is not None and not hasattr(slidermin, 'val'): + raise ValueError("Argument slidermin ({}) has no 'val'" + .format(type(slidermin))) + if slidermax is not None and not hasattr(slidermax, 'val'): + raise ValueError("Argument slidermax ({}) has no 'val'" + .format(type(slidermax))) + self.closedmin = closedmin + self.closedmax = closedmax + self.slidermin = slidermin + self.slidermax = slidermax + self.drag_active = False + self.valmin = valmin + self.valmax = valmax + self.valstep = valstep + valinit = self._value_in_bounds(valinit) + if valinit is None: + valinit = valmin + self.val = valinit + self.valinit = valinit + self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs) + self.vline = ax.axvline(valinit, 0, 1, color='r', lw=1) + + self.valfmt = valfmt + ax.set_yticks([]) + ax.set_xlim((valmin, valmax)) + ax.set_xticks([]) + ax.set_navigate(False) + + self.connect_event('button_press_event', self._update) + self.connect_event('button_release_event', self._update) + if dragging: + self.connect_event('motion_notify_event', self._update) + self.label = ax.text(0.5, -0.5, label, transform=ax.transAxes, + verticalalignment='center', + horizontalalignment='center') + self.mintext = ax.text(-0.02, 0.5, valfmt % valmin, + transform=ax.transAxes, + verticalalignment='center', + horizontalalignment='right') + self.maxtext = ax.text(1.02, 0.5, valfmt % valmax, + transform=ax.transAxes, + verticalalignment='center', + horizontalalignment='left') + self.valtext = ax.text(0.5, 0.45, valfmt % valinit, + transform=ax.transAxes, + verticalalignment='center', + horizontalalignment='center', + weight='bold') + + self.cnt = 0 + self.observers = {} + + self.set_val(valinit) + + def _value_in_bounds(self, val): + """ Makes sure self.val is with given bounds.""" + if self.valstep: + val = np.round((val - self.valmin)/self.valstep)*self.valstep + val += self.valmin + + if val <= self.valmin: + if not self.closedmin: + return + val = self.valmin + elif val >= self.valmax: + if not self.closedmax: + return + val = self.valmax + + if self.slidermin is not None and val <= self.slidermin.val: + if not self.closedmin: + return + val = self.slidermin.val + + if self.slidermax is not None and val >= self.slidermax.val: + if not self.closedmax: + return + val = self.slidermax.val + return val + + def _update(self, event): + """update the slider position""" + if self.ignore(event): + return + + if event.button != 1: + return + + if event.name == 'button_press_event' and event.inaxes == self.ax: + self.drag_active = True + event.canvas.grab_mouse(self.ax) + + if not self.drag_active: + return + + elif ((event.name == 'button_release_event') or + (event.name == 'button_press_event' and + event.inaxes != self.ax)): + self.drag_active = False + event.canvas.release_mouse(self.ax) + return + val = self._value_in_bounds(event.xdata) + if (val is not None) and (val != self.val): + self.set_val(val) + + def set_val(self, val): + """ + Set slider value to *val* + Parameters + ---------- + val : float + """ + xy = self.poly.xy + xy[2] = val, 1 + xy[3] = val, 0 + self.poly.xy = xy + self.valtext.set_text(self.valfmt % val) + if self.drawon: + self.ax.figure.canvas.draw_idle() + # stratcanv = fig.canvas.copy_from_bbox(ax.bbox) + # fig.canvas.restore_region(stratcanv) + self.val = val + if not self.eventson: + return + for cid, func in six.iteritems(self.observers): + func(val) + + def on_changed(self, func): + """ + When the slider value is changed call *func* with the new + slider value + Parameters + ---------- + func : callable + Function to call when slider is changed. + The function must accept a single float as its arguments. + Returns + ------- + cid : int + Connection id (which can be used to disconnect *func*) + """ + cid = self.cnt + self.observers[cid] = func + self.cnt += 1 + return cid + + def disconnect(self, cid): + """ + Remove the observer with connection id *cid* + Parameters + ---------- + cid : int + Connection id of the observer to be removed + """ + try: + del self.observers[cid] + except KeyError: + pass + + def reset(self): + """Reset the slider to the initial value""" + if (self.val != self.valinit): + self.set_val(self.valinit) + + +class NoDrawButton(AxesWidget): + """ + A GUI neutral button. + For the button to remain responsive you must keep a reference to it. + Call :meth:`on_clicked` to connect to the button. + Attributes + ---------- + ax : + The :class:`matplotlib.axes.Axes` the button renders into. + label : + A :class:`matplotlib.text.Text` instance. + color : + The color of the button when not hovering. + hovercolor : + The color of the button when hovering. + """ + + def __init__(self, ax, label, image=None, + color='0.85', hovercolor='0.95'): + """ + Parameters + ---------- + ax : matplotlib.axes.Axes + The :class:`matplotlib.axes.Axes` instance the button + will be placed into. + label : str + The button text. Accepts string. + image : array, mpl image, Pillow Image + The image to place in the button, if not *None*. + Can be any legal arg to imshow (numpy array, + matplotlib Image instance, or Pillow Image). + color : color + The color of the button when not activated + hovercolor : color + The color of the button when the mouse is over it + """ + AxesWidget.__init__(self, ax) + + if image is not None: + ax.imshow(image) + self.label = ax.text(0.5, 0.5, label, + verticalalignment='center', + horizontalalignment='center', + transform=ax.transAxes) + + self.cnt = 0 + self.observers = {} + + self.connect_event('button_press_event', self._click) + self.connect_event('button_release_event', self._release) + # self.connect_event('motion_notify_event', self._motion) + ax.set_navigate(False) + ax.set_facecolor(color) + ax.set_xticks([]) + ax.set_yticks([]) + self.color = color + self.hovercolor = hovercolor + + self._lastcolor = color + + def _click(self, event): + if self.ignore(event): + return + if event.inaxes != self.ax: + return + if not self.eventson: + return + if event.canvas.mouse_grabber != self.ax: + event.canvas.grab_mouse(self.ax) + + def _release(self, event): + if self.ignore(event): + return + if event.canvas.mouse_grabber != self.ax: + return + event.canvas.release_mouse(self.ax) + if not self.eventson: + return + if event.inaxes != self.ax: + return + for cid, func in self.observers.items(): + func(event) + + # def _motion(self, event): + # if self.ignore(event): + # return + # if event.inaxes == self.ax: + # c = self.hovercolor + # return + # else: + # c = self.color + # if c != self._lastcolor: + # self.ax.set_facecolor(c) + # self._lastcolor = c + # if self.drawon: + # self.ax.figure.canvas.draw() + + def on_clicked(self, func): + """ + When the button is clicked, call this *func* with event. + A connection id is returned. It can be used to disconnect + the button from its callback. + """ + cid = self.cnt + self.observers[cid] = func + self.cnt += 1 + return cid + + def disconnect(self, cid): + """remove the observer with connection id *cid*""" + try: + del self.observers[cid] + except KeyError: + pass \ No newline at end of file From 6a642de0b05ba3bbf4e34a22e49eba6cd2bbcddd Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 15:03:49 -0500 Subject: [PATCH 02/12] add model desc to readme --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 63aa3d4..46afedd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # rivers2stratigraphy -Explore how a river becomes stratigraphy. - +Explore how a river becomes stratigraphy demo_gif @@ -13,6 +12,13 @@ This repository is also linked into the [SedEdu suite of education modules](http +## About the model +Stratigraphic model based on LAB models, i.e., geometric channel body is deposited in "matrix" of floodplain mud. +The channel is always fixed to the basin surface and subsidence is only control on vertical stratigraphy. +Horizontal stratigraphy is set by 1) lateral migration (drawn from a pdf) and dampened for realism, and 2) avulsion that is set to a fixed value. + + + ## Installation and running the module Visit the section of the text below for more information on installing and executing the `rivers2stratigraphy` program on your computer. From 5dff19db7068a10dfafd89bb2ccb8ab57973aafc Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 16:08:45 -0500 Subject: [PATCH 03/12] remove unneeded imports --- rivers2stratigraphy/channel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rivers2stratigraphy/channel.py b/rivers2stratigraphy/channel.py index f6d970e..700848d 100644 --- a/rivers2stratigraphy/channel.py +++ b/rivers2stratigraphy/channel.py @@ -1,7 +1,6 @@ import numpy as np from matplotlib.patches import Polygon, Rectangle -from matplotlib.collections import PatchCollection, LineCollection import shapely.geometry as sg import shapely.ops as so From 0d9d43e16508a3ada14dc947e0bd61aa12039b29 Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 16:09:29 -0500 Subject: [PATCH 04/12] move out the widgets and add reset button functions --- rivers2stratigraphy/utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rivers2stratigraphy/utils.py b/rivers2stratigraphy/utils.py index d656d9d..2e6ed3d 100644 --- a/rivers2stratigraphy/utils.py +++ b/rivers2stratigraphy/utils.py @@ -1,8 +1,4 @@ # utilities for drawing the gui etc -import numpy as np -from matplotlib.widgets import AxesWidget -import six - def format_number(number): integer = int(round(number, -1)) @@ -24,4 +20,15 @@ def normalizeColor(v, minV, maxV): return (v-minV)/(maxV-minV) +def strat_reset(event, gui): + gui.strat.Bast = 0 + gui.strat.channelBodyList = [] + +def slide_reset(event, gui): + gui.sm.slide_Qw.reset() + gui.sm.slide_sig.reset() + gui.sm.slide_Ta.reset() + gui.sm.rad_col.set_active(0) + gui.sm.slide_yView.reset() + gui.sm.slide_Bb.reset() \ No newline at end of file From 8917e720ecf2eded36f0355fb5cce5e2049c6ddb Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 16:10:04 -0500 Subject: [PATCH 05/12] rename main.py as gui.py to separate logic of gui launch --- rivers2stratigraphy/gui.py | 148 ++++++++++++++++ rivers2stratigraphy/main.py | 335 ------------------------------------ 2 files changed, 148 insertions(+), 335 deletions(-) create mode 100644 rivers2stratigraphy/gui.py delete mode 100644 rivers2stratigraphy/main.py diff --git a/rivers2stratigraphy/gui.py b/rivers2stratigraphy/gui.py new file mode 100644 index 0000000..b86a28d --- /dev/null +++ b/rivers2stratigraphy/gui.py @@ -0,0 +1,148 @@ +""" +rivers2stratigraphy GUI -- build river stratigraphy interactively + Stratigraphic model based on LAB models, i.e., geometric channel body is + deposited in "matrix" of floodplain mud. The channel is always fixed to the + basin surface and subsidence is only control on vertical stratigraphy. + Horizontal stratigraphy is set by 1) lateral migration (drawn from a pdf) + and dampened for realism, and 2) avulsion that is set to a fixed value. + + written by Andrew J. Moodie + amoodie@rice.edu + Feb 2018 + + TODO: + - control for "natural" ad default where lateral migration + and Ta are a function of sediment transport (Qw) + + + +""" + + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation + +from strat import Strat +from slider_manager import SliderManager +from channel import ActiveChannel, State, ChannelBody +import geom, sedtrans, utils + +class Config: + """ + dummy config class for storing info + """ + + pass + + + +class GUI(object): + + """ + main GUI object that selects parameters for initialization and + handles creation of all the needed parts of the model. This class is + initialized below by class Runner if this file is run as __main__ + """ + + def __init__(self): + # initial conditions + + config = Config() + + # model run params + config.dt = 100 # timestep in yrs + self._paused = False + + # setup params + config.Cf = 0.004 # friction coeff + config.D50 = 300*1e-6 + config.Beta = 1.5 # exponent to avulsion function + config.Df = 0.6 # dampening factor to lateral migration rate change + config.dxdtstd = 1 # stdev of lateral migration dist, [m/yr]? + + # constants + config.conR = 1.65 + config.cong = 9.81 + config.conrhof = 1000 + config.connu = 1.004e-6 + config.Rep = geom.Repfun(config.D50, config.conR, config.cong, config.connu) # particle Reynolds num + + # water discharge slider params + config.Qw = config.Qwinit = 1000 + config.Qwmin = 200 + config.Qwmax = 4000 + config.Qwstep = 100 + + # subsidence slider params + config.sig = config.siginit = 2 + config.sigmin = 0 + config.sigmax = 5 + config.sigstep = 0.2 + + # avulsion timescale slider params + config.Ta = config.Tainit = 500 + config.Tamin = config.dt + config.Tamax = 1500 + config.Tastep = 10 + + # yView slider params + config.yView = config.yViewinit = 100 + config.yViewmin = 25 + config.yViewmax = 250 + config.yViewstep = 25 + + # basin width slider params + config.Bb = config.Bbinit = 4000 # width of belt (m) + config.Bbmin = 1 + config.Bbmax = 10 + config.Bbstep = 0.5 + + # additional initializations + config.Bast = 0 # Basin top level + + # setup the figure + plt.rcParams['toolbar'] = 'None' + plt.rcParams['figure.figsize'] = 8, 6 + self.fig, self.strat_ax = plt.subplots() + self.fig.canvas.set_window_title('SedEdu -- rivers2stratigraphy') + plt.subplots_adjust(left=0.085, bottom=0.1, top=0.95, right=0.5) + self.strat_ax.set_xlabel("channel belt (km)") + self.strat_ax.set_ylabel("stratigraphy (m)") + plt.ylim(-config.yView, 0.1*config.yView) + plt.xlim(-config.Bb/2, config.Bb/2) + self.strat_ax.xaxis.set_major_formatter( plt.FuncFormatter( + lambda v, x: str(v / 1000).format('%0.0f')) ) + + # add sliders + self.config = config + self.sm = SliderManager(self) + + + def pause_anim(self, event): + """ + pause animation by altering hidden var + """ + if self._paused: + self._paused = False + else: + self._paused = True + + +class Runner(object): + gui = GUI() + + # time looping + gui.strat = Strat(gui) + + anim = animation.FuncAnimation(gui.fig, gui.strat, + interval=100, blit=False, + save_count=None) + + plt.show() + + + +if __name__ == "__main__": + runner = Runner() + diff --git a/rivers2stratigraphy/main.py b/rivers2stratigraphy/main.py deleted file mode 100644 index e5bc8f2..0000000 --- a/rivers2stratigraphy/main.py +++ /dev/null @@ -1,335 +0,0 @@ -""" -rivers2stratigraphy GUI -- build river stratigraphy interactively - Stratigraphic model based on LAB models, i.e., geometric channel body is - deposited in "matrix" of floodplain mud. The channel is always fixed to the - basin surface and subsidence is only control on vertical stratigraphy. - Horizontal stratigraphy is set by 1) lateral migration (drawn from a pdf) - and dampened for realism, and 2) avulsion that is set to a fixed value. - - written by Andrew J. Moodie - amoodie@rice.edu - Feb 2018 - - TODO: - - control for "natural" ad default where lateral migration - and Ta are a function of sediment transport (Qw) - - - -""" - - -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.widgets as widget -from matplotlib.patches import Polygon, Rectangle -from matplotlib.collections import PatchCollection, LineCollection -import matplotlib.animation as animation -import shapely.geometry as sg -import shapely.ops as so -from itertools import compress -import sys -import gc - -# import channel -from .channel import ActiveChannel, State, ChannelBody -import geom, sedtrans, utils - - -# model run params -dt = 100 # timestep in yrs - -# setup params -Cf = 0.004 # friction coeff -D50 = 300*1e-6 -Beta = 1.5 # exponent to avulsion function -Df = 0.6 # dampening factor to lateral migration rate change -dxdtstd = 1 # stdev of lateral migration dist, [m/yr]? - -conR = 1.65 -cong = 9.81 -conrhof = 1000 -connu = 1.004e-6 -Rep = geom.Repfun(D50, conR, cong, connu) # particle Reynolds num - -# initial conditions -Bb = BbInit = 4000 # width of belt (m) -yView = yViewInit = 100 -Qw = QwInit = 1000 -Bast = 0 # Basin top level - - -class SliderManager(object): - def __init__(self): - # read the sliders for values - self.get_all() - self.D50 = D50 - self.cong = cong - self.Rep = Rep - self.dt = dt - self.Df = Df - self.dxdtstd = dxdtstd - - def get_display_options(self): - self.colFlag = col_dict[rad_col.value_selected] - self.yView = slide_yView.val - - def get_calculation_options(self): - self.Bb = slide_Bb.val * 1000 - self.Qw = slide_Qw.val - self.sig = slide_sig.val / 1000 - self.Ta = slide_Ta.val - - def get_all(self): - self.get_display_options() - self.get_calculation_options() - - - -class Strat(object): - - def __init__(self, ax): - ''' - initiation of the main strat object - ''' - - self.ax = ax - self.Bast = 0 - self.avul_num = 0 - self.color = False - self.sm = SliderManager() - - # create an active channel and corresponding PatchCollection - self.activeChannel = ActiveChannel(x_centi = 0, Bast = self.Bast, age = 0, - avul_num = 0, sm = self.sm) - self.activeChannelPatchCollection = PatchCollection(self.activeChannel.patches) - - # create a channelbody and corresponding PatchCollection - self.channelBodyList = [] - self.channelBodyPatchCollection = PatchCollection(self.channelBodyList) - - # add PatchCollestions - self.ax.add_collection(self.channelBodyPatchCollection) - self.ax.add_collection(self.activeChannelPatchCollection) - - # set fixed color attributes of PatchCollections - self.channelBodyPatchCollection.set_edgecolor('0') - self.activeChannelPatchCollection.set_facecolor('0.6') - self.activeChannelPatchCollection.set_edgecolor('0') - - self.BastLine, = self.ax.plot([-Bbmax*1000/2, Bbmax*1000/2], - [self.Bast, self.Bast], 'k--', animated=False) # plot basin top - self.VE_val = plt.text(0.675, 0.025, 'VE = ' + str(round(self.sm.Bb/self.sm.yView, 1)), - fontsize=12, transform=ax.transAxes, - backgroundcolor='white') - - - def func_init(fig, ax, self): - ''' - handles the initiation of the figure and axes for blitting - ''' - - return ax, self - - - def __call__(self, i): - ''' - called every loop - ''' - - # find new slider vals - self.sm.get_all() - - if anim.running: - # timestep the current channel objects - dz = self.sm.sig * dt - for c in self.channelBodyList: - c.subside(dz) - - if not self.activeChannel.avulsed: - # when an avulsion has not occurred: - self.activeChannel.timestep() - - else: - # once an avulsion has occurred: - self.channelBodyList.append( ChannelBody(self.activeChannel) ) - self.avul_num += 1 - self.color = True - - # create a new Channel - self.activeChannel = ActiveChannel(Bast = self.Bast, age = i, - avul_num = self.avul_num, sm = self.sm) - - # remove outdated channels - stratMin = self.Bast - yViewmax - outdatedIdx = [c.polygonYs.max() < stratMin for c in self.channelBodyList] - self.channelBodyList = [c for (c, i) in - zip(self.channelBodyList, outdatedIdx) if not i] - - # generate new patch lists for updating the PatchCollection objects - activeChannelPatches = [Rectangle(s.ll, s.Bc, s.H) for s - in iter(self.activeChannel.stateList)] - self.channelBodyPatchList = [c.get_patch() for c in self.channelBodyList] - - # set paths of the PatchCollection Objects - self.channelBodyPatchCollection.set_paths(self.channelBodyPatchList) - self.activeChannelPatchCollection.set_paths(activeChannelPatches) - - # self.qs = sedtrans.qsEH(D50, Cf, - # sedtrans.taubfun(self.channel.H, self.channel.S, cong, conrhof), - # conR, cong, conrhof) # sedment transport rate based on new geom - - # update plot - if self.color: - if self.sm.colFlag == 'age': - age_array = np.array([c.age for c in self.channelBodyList]) - if age_array.size > 0: - self.channelBodyPatchCollection.set_array(age_array) - self.channelBodyPatchCollection.set_clim(vmin=age_array.min(), vmax=age_array.max()) - self.channelBodyPatchCollection.set_cmap(plt.cm.viridis) - elif self.sm.colFlag == 'Qw': - self.channelBodyPatchCollection.set_array(np.array([c.Qw for c in self.channelBodyList])) - self.channelBodyPatchCollection.set_clim(vmin=Qwmin, vmax=Qwmax) - self.channelBodyPatchCollection.set_cmap(plt.cm.viridis) - elif self.sm.colFlag == 'avul': - self.channelBodyPatchCollection.set_array(np.array([c.avul_num % 9 for c in self.channelBodyList])) - self.channelBodyPatchCollection.set_clim(vmin=0, vmax=9) - self.channelBodyPatchCollection.set_cmap(plt.cm.Set1) - elif self.sm.colFlag == 'sig': - sig_array = np.array([c.sig for c in self.channelBodyList]) - self.channelBodyPatchCollection.set_array(sig_array) - self.channelBodyPatchCollection.set_clim(vmin=sigmin/1000, vmax=sigmax/1000) - self.channelBodyPatchCollection.set_cmap(plt.cm.viridis) - - # yview and xview - ylims = utils.new_ylims(yView = self.sm.yView, Bast = self.Bast) - self.ax.set_ylim(ylims) - self.ax.set_xlim(-self.sm.Bb/2, self.sm.Bb/2) - - # vertical exagg text - if i % 10 == 0: - self.axbbox = self.ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) - width, height = self.axbbox.width, self.axbbox.height - self.VE_val.set_text('VE = ' + str(round((self.sm.Bb/width)/(self.sm.yView/height), 1))) - - return self.BastLine, self.VE_val, \ - self.channelBodyPatchCollection, self.activeChannelPatchCollection - - - -# setup the figure -plt.rcParams['toolbar'] = 'None' -plt.rcParams['figure.figsize'] = 8, 6 -fig, ax = plt.subplots() -fig.canvas.set_window_title('SedEdu -- rivers2stratigraphy') -plt.subplots_adjust(left=0.085, bottom=0.1, top=0.95, right=0.5) -ax.set_xlabel("channel belt (km)") -ax.set_ylabel("stratigraphy (m)") -plt.ylim(-yViewInit, 0.1*yViewInit) -plt.xlim(-Bb/2, Bb/2) -ax.xaxis.set_major_formatter( plt.FuncFormatter( - lambda v, x: str(v / 1000).format('%0.0f')) ) - - - -# define reset functions, must operate on global vars -def slide_reset(event): - slide_Qw.reset() - slide_sig.reset() - slide_Ta.reset() - rad_col.set_active(0) - slide_yView.reset() - slide_Bb.reset() - - -def axis_reset(event): - strat.Bast = 0 - strat.channelBodyList = [] - - -def pause_anim(event): - if anim.running: - anim.running = False - else: - anim.running = True - - - -# add sliders -widget_color = 'lightgoldenrodyellow' - -QwInit = QwInit -Qwmin = 200 -Qwmax = 4000 -Qwstep = 100 -slide_Qw_ax = plt.axes([0.565, 0.875, 0.36, 0.05], facecolor=widget_color) -slide_Qw = utils.MinMaxSlider(slide_Qw_ax, 'water discharge (m$^3$/s)', Qwmin, Qwmax, -valinit=QwInit, valstep=Qwstep, valfmt="%0.0f", transform=ax.transAxes) -# slide_Qw.on_changed(redraw_strat) - -sigInit = 2 -sigmin = 0 -sigmax = 5 -sigstep = 0.2 -slide_sig_ax = plt.axes([0.565, 0.770, 0.36, 0.05], facecolor=widget_color) -slide_sig = utils.MinMaxSlider(slide_sig_ax, 'subsidence (mm/yr)', sigmin, sigmax, -valinit=sigInit, valstep=sigstep, valfmt="%g", transform=ax.transAxes) - -TaInit = 500 -Tamin = dt -Tamax = 1500 -slide_Ta_ax = plt.axes([0.565, 0.665, 0.36, 0.05], facecolor=widget_color) -slide_Ta = utils.MinMaxSlider(slide_Ta_ax, 'avulsion timescale (yr)', Tamin, Tamax, -valinit=TaInit, valstep=10, valfmt="%i", transform=ax.transAxes) -avulCmap = plt.cm.Set1(range(9)) - -rad_col_ax = plt.axes([0.565, 0.45, 0.225, 0.15], facecolor=widget_color) -rad_col = widget.RadioButtons(rad_col_ax, ('Deposit age', 'Water discharge', 'Subsidence rate', 'Avulsion number')) - -yViewInit = yViewInit -yViewmin = 25 -yViewmax = 250 -slide_yView_ax = plt.axes([0.565, 0.345, 0.36, 0.05], facecolor=widget_color) -slide_yView = utils.MinMaxSlider(slide_yView_ax, 'stratigraphic view (m)', yViewmin, yViewmax, -valinit=yViewInit, valstep=25, valfmt="%i", transform=ax.transAxes) - -BbInit = BbInit # width of belt -Bbmin = 1 -Bbmax = 10 -slide_Bb_ax = plt.axes([0.565, 0.24, 0.36, 0.05], facecolor=widget_color) -slide_Bb = utils.MinMaxSlider(slide_Bb_ax, 'Channel belt width (km)', Bbmin, Bbmax, -valinit=BbInit/1000, valstep=0.5, valfmt="%g", transform=ax.transAxes) - - - -btn_slidereset_ax = plt.axes([0.565, 0.14, 0.2, 0.04]) -btn_slidereset = utils.NoDrawButton(btn_slidereset_ax, 'Reset sliders', color=widget_color, hovercolor='0.975') -btn_slidereset.on_clicked(slide_reset) - -btn_axisreset_ax = plt.axes([0.565, 0.09, 0.2, 0.04]) -btn_axisreset = utils.NoDrawButton(btn_axisreset_ax, 'Reset stratigraphy', color=widget_color, hovercolor='0.975') -btn_axisreset.on_clicked(axis_reset) - -btn_pause_ax = plt.axes([0.565, 0.03, 0.2, 0.04]) -btn_pause = utils.NoDrawButton(btn_pause_ax, 'Pause', color=widget_color, hovercolor='0.975') -btn_pause.on_clicked(pause_anim) - - - -# initialize a few more things -col_dict = {'Water discharge': 'Qw', - 'Avulsion number': 'avul', - 'Deposit age': 'age', - 'Subsidence rate':'sig'} - - -# time looping -strat = Strat(ax) - -anim = animation.FuncAnimation(fig, strat, interval=100, blit=False, save_count=None) -anim.running = True - -plt.show() - -if __name__ == "__main__": - pass From f639d3414495b9fa82506e528ae59ba664e22290 Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 16:22:28 -0500 Subject: [PATCH 06/12] shift config class to utils --- rivers2stratigraphy/gui.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rivers2stratigraphy/gui.py b/rivers2stratigraphy/gui.py index b86a28d..06a089e 100644 --- a/rivers2stratigraphy/gui.py +++ b/rivers2stratigraphy/gui.py @@ -28,12 +28,7 @@ from channel import ActiveChannel, State, ChannelBody import geom, sedtrans, utils -class Config: - """ - dummy config class for storing info - """ - pass @@ -48,7 +43,7 @@ class GUI(object): def __init__(self): # initial conditions - config = Config() + config = utils.Config() # model run params config.dt = 100 # timestep in yrs From 85f921278d68247342ac0f6d51933c054217268b Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 16:22:48 -0500 Subject: [PATCH 07/12] widgets --- rivers2stratigraphy/widgets.py | 141 ++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 3 deletions(-) diff --git a/rivers2stratigraphy/widgets.py b/rivers2stratigraphy/widgets.py index 62398a3..66221f8 100644 --- a/rivers2stratigraphy/widgets.py +++ b/rivers2stratigraphy/widgets.py @@ -1,6 +1,11 @@ -# import numpy as np +""" +module to provide customized versions of the matplotlib widgets +""" + +import numpy as np from matplotlib.widgets import AxesWidget -# import six +from matplotlib.patches import Circle +import six class MinMaxSlider(AxesWidget): """ @@ -332,4 +337,134 @@ def disconnect(self, cid): try: del self.observers[cid] except KeyError: - pass \ No newline at end of file + pass + + +class RadioButtons(AxesWidget): + """ + A GUI neutral radio button. + For the buttons to remain responsive + you must keep a reference to this object. + The following attributes are exposed: + *ax* + The :class:`matplotlib.axes.Axes` instance the buttons are in + *activecolor* + The color of the button when clicked + *labels* + A list of :class:`matplotlib.text.Text` instances + *circles* + A list of :class:`matplotlib.patches.Circle` instances + *value_selected* + A string listing the current value selected + Connect to the RadioButtons with the :meth:`on_clicked` method + """ + def __init__(self, ax, labels, active=0, activecolor='blue'): + """ + Add radio buttons to :class:`matplotlib.axes.Axes` instance *ax* + *labels* + A len(buttons) list of labels as strings + *active* + The index into labels for the button that is active + *activecolor* + The color of the button when clicked + """ + AxesWidget.__init__(self, ax) + self.activecolor = activecolor + self.value_selected = None + + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_navigate(False) + dy = 1. / (len(labels) + 1) + ys = np.linspace(1 - dy, dy, len(labels)) + cnt = 0 + axcolor = ax.get_facecolor() + + # scale the radius of the circle with the spacing between each one + circle_radius = (dy / 2) - 0.01 + + # defaul to hard-coded value if the radius becomes too large + if(circle_radius > 0.05): + circle_radius = 0.05 + + self.labels = [] + self.circles = [] + for y, label in zip(ys, labels): + t = ax.text(0.25, y, label, transform=ax.transAxes, + horizontalalignment='left', + verticalalignment='center') + + if cnt == active: + self.value_selected = label + facecolor = activecolor + else: + facecolor = axcolor + + p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black', + facecolor=facecolor, transform=ax.transAxes) + + self.labels.append(t) + self.circles.append(p) + ax.add_patch(p) + cnt += 1 + + self.connect_event('button_press_event', self._clicked) + + self.cnt = 0 + self.observers = {} + + def _clicked(self, event): + if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: + return + xy = self.ax.transAxes.inverted().transform_point((event.x, event.y)) + pclicked = np.array([xy[0], xy[1]]) + for i, (p, t) in enumerate(zip(self.circles, self.labels)): + if (t.get_window_extent().contains(event.x, event.y) + or np.linalg.norm(pclicked - p.center) < p.radius): + self.set_active(i) + break + + def set_active(self, index): + """ + Trigger which radio button to make active. + *index* is an index into the original label list + that this object was constructed with. + Raise ValueError if the index is invalid. + Callbacks will be triggered if :attr:`eventson` is True. + """ + if 0 > index >= len(self.labels): + raise ValueError("Invalid RadioButton index: %d" % index) + + self.value_selected = self.labels[index].get_text() + + for i, p in enumerate(self.circles): + if i == index: + color = self.activecolor + else: + color = self.ax.get_facecolor() + p.set_facecolor(color) + + if self.drawon: + self.ax.figure.canvas.draw() + + if not self.eventson: + return + for cid, func in self.observers.items(): + func(self.labels[index].get_text()) + + def on_clicked(self, func): + """ + When the button is clicked, call *func* with button label + A connection id is returned which can be used to disconnect + """ + cid = self.cnt + self.observers[cid] = func + self.cnt += 1 + return cid + + def disconnect(self, cid): + """remove the observer with connection id *cid*""" + try: + del self.observers[cid] + except KeyError: + pass From 0d959a726c38f7279e1250ad56a77d069d8f0a8d Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 16:23:03 -0500 Subject: [PATCH 08/12] move strat and slider manager to own classes --- rivers2stratigraphy/slider_manager.py | 92 +++++++++++++++++ rivers2stratigraphy/strat.py | 141 ++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 rivers2stratigraphy/slider_manager.py create mode 100644 rivers2stratigraphy/strat.py diff --git a/rivers2stratigraphy/slider_manager.py b/rivers2stratigraphy/slider_manager.py new file mode 100644 index 0000000..84051ec --- /dev/null +++ b/rivers2stratigraphy/slider_manager.py @@ -0,0 +1,92 @@ +import matplotlib.pyplot as plt + +import widgets +import utils + +class SliderManager(object): + def __init__(self, gui): + + widget_color = 'lightgoldenrodyellow' + + # inputs of ranges to initialize + slide_Qw_ax = plt.axes([0.565, 0.875, 0.36, 0.05], facecolor=widget_color) + self.slide_Qw = widgets.MinMaxSlider(slide_Qw_ax, 'water discharge (m$^3$/s)', + gui.config.Qwmin, gui.config.Qwmax, + valinit=gui.config.Qwinit, valstep=gui.config.Qwstep, + valfmt="%0.0f", transform=gui.strat_ax.transAxes) + + slide_sig_ax = plt.axes([0.565, 0.770, 0.36, 0.05], facecolor=widget_color) + self.slide_sig = widgets.MinMaxSlider(slide_sig_ax, 'subsidence (mm/yr)', + gui.config.sigmin, gui.config.sigmax, + valinit=gui.config.siginit, valstep=gui.config.sigstep, + valfmt="%g", transform=gui.strat_ax.transAxes) + + slide_Ta_ax = plt.axes([0.565, 0.665, 0.36, 0.05], facecolor=widget_color) + self.slide_Ta = widgets.MinMaxSlider(slide_Ta_ax, 'avulsion timescale (yr)', + gui.config.Tamin, gui.config.Tamax, + valinit=gui.config.Tainit, valstep=gui.config.Tastep, + valfmt="%i", transform=gui.strat_ax.transAxes) + + rad_col_ax = plt.axes([0.565, 0.45, 0.225, 0.15], facecolor=widget_color) + self.rad_col = widgets.RadioButtons(rad_col_ax, ('Deposit age', + 'Water discharge', + 'Subsidence rate', + 'Avulsion number')) + + slide_yView_ax = plt.axes([0.565, 0.345, 0.36, 0.05], facecolor=widget_color) + self.slide_yView = widgets.MinMaxSlider(slide_yView_ax, 'stratigraphic view (m)', + gui.config.yViewmin, gui.config.yViewmax, + valinit=gui.config.yViewinit, valstep=gui.config.yViewstep, + valfmt="%i", transform=gui.strat_ax.transAxes) + + slide_Bb_ax = plt.axes([0.565, 0.24, 0.36, 0.05], facecolor=widget_color) + self.slide_Bb = widgets.MinMaxSlider(slide_Bb_ax, 'Channel belt width (km)', + gui.config.Bbmin, gui.config.Bbmax, + valinit=gui.config.Bbinit/1000, valstep=gui.config.Bbstep, + valfmt="%g", transform=gui.strat_ax.transAxes) + + btn_slidereset_ax = plt.axes([0.565, 0.14, 0.2, 0.04]) + self.btn_slidereset = widgets.NoDrawButton(btn_slidereset_ax, 'Reset sliders', color=widget_color, hovercolor='0.975') + # self.btn_slidereset.on_clicked(gui.slide_reset) + self.btn_slidereset.on_clicked(lambda x: utils.slide_reset(event=x, gui=gui)) + + btn_axisreset_ax = plt.axes([0.565, 0.09, 0.2, 0.04]) + self.btn_axisreset = widgets.NoDrawButton(btn_axisreset_ax, 'Reset stratigraphy', color=widget_color, hovercolor='0.975') + # self.btn_axisreset.on_clicked(gui.strat_reset) + self.btn_axisreset.on_clicked(lambda x: utils.strat_reset(event=x, gui=gui)) + + btn_pause_ax = plt.axes([0.565, 0.03, 0.2, 0.04]) + self.btn_pause = widgets.NoDrawButton(btn_pause_ax, 'Pause', color=widget_color, hovercolor='0.975') + self.btn_pause.on_clicked(gui.pause_anim) + + # initialize a few more things + self.col_dict = {'Water discharge': 'Qw', + 'Avulsion number': 'avul', + 'Deposit age': 'age', + 'Subsidence rate':'sig'} + + # read the sliders for values + self.get_all() + self.D50 = gui.config.D50 + self.cong = gui.config.cong + self.Rep = gui.config.Rep + self.dt = gui.config.dt + self.Df = gui.config.Df + self.Bast = gui.config.Bast + self.dxdtstd = gui.config.dxdtstd + self.Bbmax = gui.config.Bbmax + self.yViewmax = gui.config.yViewmax + + def get_display_options(self): + self.colFlag = self.col_dict[self.rad_col.value_selected] + self.yView = self.slide_yView.val + + def get_calculation_options(self): + self.Bb = self.slide_Bb.val * 1000 + self.Qw = self.slide_Qw.val + self.sig = self.slide_sig.val / 1000 + self.Ta = self.slide_Ta.val + + def get_all(self): + self.get_display_options() + self.get_calculation_options() \ No newline at end of file diff --git a/rivers2stratigraphy/strat.py b/rivers2stratigraphy/strat.py new file mode 100644 index 0000000..9d8369b --- /dev/null +++ b/rivers2stratigraphy/strat.py @@ -0,0 +1,141 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Polygon, Rectangle +from matplotlib.collections import PatchCollection, LineCollection +import matplotlib.animation as animation +import shapely.geometry as sg +import shapely.ops as so + +from channel import ActiveChannel, State, ChannelBody +import utils + +class Strat(object): + + def __init__(self, gui): + ''' + initiation of the main strat object + ''' + + self.gui = gui + # self.gui.strat_ax = gui.strat_ax + self.fig = gui.fig + + self.sm = gui.sm + self.config = gui.config + + self.Bast = self.sm.Bast + + self.avul_num = 0 + self.color = False + self.avulCmap = plt.cm.Set1(range(9)) + + # self._paused = gui._paused + + # create an active channel and corresponding PatchCollection + self.activeChannel = ActiveChannel(x_centi = 0, Bast = self.Bast, age = 0, + avul_num = 0, sm = self.sm) + self.activeChannelPatchCollection = PatchCollection(self.activeChannel.patches) + + # create a channelbody and corresponding PatchCollection + self.channelBodyList = [] + self.channelBodyPatchCollection = PatchCollection(self.channelBodyList) + + # add PatchCollestions + self.gui.strat_ax.add_collection(self.channelBodyPatchCollection) + self.gui.strat_ax.add_collection(self.activeChannelPatchCollection) + + # set fixed color attributes of PatchCollections + self.channelBodyPatchCollection.set_edgecolor('0') + self.activeChannelPatchCollection.set_facecolor('0.6') + self.activeChannelPatchCollection.set_edgecolor('0') + + self.BastLine, = self.gui.strat_ax.plot([-self.sm.Bbmax*1000/2, gui.sm.Bbmax*1000/2], + [self.Bast, self.Bast], 'k--', animated=False) # plot basin top + self.VE_val = plt.text(0.675, 0.025, 'VE = ' + str(round(self.sm.Bb/self.sm.yView, 1)), + fontsize=12, transform=self.gui.strat_ax.transAxes, + backgroundcolor='white') + + + def __call__(self, i): + ''' + called every loop + ''' + + # find new slider vals + self.sm.get_all() + + if not self.gui._paused: + # timestep the current channel objects + dz = self.sm.sig * self.sm.dt + for c in self.channelBodyList: + c.subside(dz) + + if not self.activeChannel.avulsed: + # when an avulsion has not occurred: + self.activeChannel.timestep() + + else: + # once an avulsion has occurred: + self.channelBodyList.append( ChannelBody(self.activeChannel) ) + self.avul_num += 1 + self.color = True + + # create a new Channel + self.activeChannel = ActiveChannel(Bast = self.Bast, age = i, + avul_num = self.avul_num, sm = self.sm) + + # remove outdated channels + stratMin = self.Bast - self.sm.yViewmax + outdatedIdx = [c.polygonYs.max() < stratMin for c in self.channelBodyList] + self.channelBodyList = [c for (c, i) in + zip(self.channelBodyList, outdatedIdx) if not i] + + # generate new patch lists for updating the PatchCollection objects + activeChannelPatches = [Rectangle(s.ll, s.Bc, s.H) for s + in iter(self.activeChannel.stateList)] + self.channelBodyPatchList = [c.get_patch() for c in self.channelBodyList] + + # set paths of the PatchCollection Objects + self.channelBodyPatchCollection.set_paths(self.channelBodyPatchList) + self.activeChannelPatchCollection.set_paths(activeChannelPatches) + + # self.qs = sedtrans.qsEH(D50, Cf, + # sedtrans.taubfun(self.channel.H, self.channel.S, cong, conrhof), + # conR, cong, conrhof) # sedment transport rate based on new geom + + # update plot + if self.color: + if self.sm.colFlag == 'age': + age_array = np.array([c.age for c in self.channelBodyList]) + if age_array.size > 0: + self.channelBodyPatchCollection.set_array(age_array) + self.channelBodyPatchCollection.set_clim(vmin=age_array.min(), vmax=age_array.max()) + self.channelBodyPatchCollection.set_cmap(plt.cm.viridis) + elif self.sm.colFlag == 'Qw': + self.channelBodyPatchCollection.set_array(np.array([c.Qw for c in self.channelBodyList])) + self.channelBodyPatchCollection.set_clim(vmin=self.config.Qwmin, vmax=self.config.Qwmax) + self.channelBodyPatchCollection.set_cmap(plt.cm.viridis) + elif self.sm.colFlag == 'avul': + self.channelBodyPatchCollection.set_array(np.array([c.avul_num % 9 for c in self.channelBodyList])) + self.channelBodyPatchCollection.set_clim(vmin=0, vmax=9) + self.channelBodyPatchCollection.set_cmap(plt.cm.Set1) + elif self.sm.colFlag == 'sig': + sig_array = np.array([c.sig for c in self.channelBodyList]) + self.channelBodyPatchCollection.set_array(sig_array) + self.channelBodyPatchCollection.set_clim(vmin=self.config.sigmin/1000, vmax=self.config.sigmax/1000) + self.channelBodyPatchCollection.set_cmap(plt.cm.viridis) + + # yview and xview + ylims = utils.new_ylims(yView = self.sm.yView, Bast = self.Bast) + self.gui.strat_ax.set_ylim(ylims) + self.gui.strat_ax.set_xlim(-self.sm.Bb/2, self.sm.Bb/2) + + # vertical exagg text + if i % 10 == 0: + self.axbbox = self.gui.strat_ax.get_window_extent().transformed(self.fig.dpi_scale_trans.inverted()) + width, height = self.axbbox.width, self.axbbox.height + self.VE_val.set_text('VE = ' + str(round((self.sm.Bb/width)/(self.sm.yView/height), 1))) + + return self.BastLine, self.VE_val, \ + self.channelBodyPatchCollection, self.activeChannelPatchCollection + From 923e8d98ea6dfa86bfb33887e9eae4041def237c Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 16:24:00 -0500 Subject: [PATCH 09/12] update utils --- rivers2stratigraphy/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rivers2stratigraphy/utils.py b/rivers2stratigraphy/utils.py index 2e6ed3d..ae2ee21 100644 --- a/rivers2stratigraphy/utils.py +++ b/rivers2stratigraphy/utils.py @@ -1,5 +1,14 @@ # utilities for drawing the gui etc + +class Config: + """ + dummy config class for storing info during generation of GUI + """ + + pass + + def format_number(number): integer = int(round(number, -1)) string = "{:,}".format(integer) From c0083fed5a3b1e432eee04e10588db840e76715e Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 22:11:53 -0500 Subject: [PATCH 10/12] adjust imports to handle both script run and executable after loading --- rivers2stratigraphy/__init__.py | 10 +++++++-- rivers2stratigraphy/_version.py | 2 +- rivers2stratigraphy/channel.py | 2 +- rivers2stratigraphy/gui.py | 31 ++++++++++++++------------- rivers2stratigraphy/slider_manager.py | 4 ++-- rivers2stratigraphy/strat.py | 4 ++-- 6 files changed, 30 insertions(+), 23 deletions(-) diff --git a/rivers2stratigraphy/__init__.py b/rivers2stratigraphy/__init__.py index 24db764..06204c1 100644 --- a/rivers2stratigraphy/__init__.py +++ b/rivers2stratigraphy/__init__.py @@ -1,2 +1,8 @@ -# from . import launcher -# launcher.import_runner() \ No newline at end of file +print('\n\n SedEdu -- rivers2stratigraphy module') + +print('\n\nTo run the activity use command:\n') +print('rivers2stratigraphy.run()\n') + +def run(): + from . import gui + gui.Runner() diff --git a/rivers2stratigraphy/_version.py b/rivers2stratigraphy/_version.py index 845be45..d1eb742 100644 --- a/rivers2stratigraphy/_version.py +++ b/rivers2stratigraphy/_version.py @@ -1 +1 @@ -__version__ = "0.2.5" \ No newline at end of file +__version__ = "0.2.6" \ No newline at end of file diff --git a/rivers2stratigraphy/channel.py b/rivers2stratigraphy/channel.py index 700848d..c9c3d70 100644 --- a/rivers2stratigraphy/channel.py +++ b/rivers2stratigraphy/channel.py @@ -4,7 +4,7 @@ import shapely.geometry as sg import shapely.ops as so -import geom, sedtrans, utils +from . import geom, sedtrans, utils class ActiveChannel(object): diff --git a/rivers2stratigraphy/gui.py b/rivers2stratigraphy/gui.py index 06a089e..049801b 100644 --- a/rivers2stratigraphy/gui.py +++ b/rivers2stratigraphy/gui.py @@ -23,13 +23,11 @@ import matplotlib.pyplot as plt import matplotlib.animation as animation -from strat import Strat -from slider_manager import SliderManager -from channel import ActiveChannel, State, ChannelBody -import geom, sedtrans, utils - - - +from .strat import Strat +# import strat +from .slider_manager import SliderManager +from .channel import ActiveChannel, State, ChannelBody +from . import geom, sedtrans, utils class GUI(object): @@ -124,20 +122,23 @@ def pause_anim(self, event): self._paused = True + class Runner(object): - gui = GUI() + def __init__(self): + gui = GUI() - # time looping - gui.strat = Strat(gui) + # time looping + gui.strat = Strat(gui) - anim = animation.FuncAnimation(gui.fig, gui.strat, - interval=100, blit=False, - save_count=None) + anim = animation.FuncAnimation(gui.fig, gui.strat, + interval=100, blit=False, + save_count=None) - plt.show() + plt.show() -if __name__ == "__main__": +if __name__ == '__main__': runner = Runner() + diff --git a/rivers2stratigraphy/slider_manager.py b/rivers2stratigraphy/slider_manager.py index 84051ec..fcb5e1f 100644 --- a/rivers2stratigraphy/slider_manager.py +++ b/rivers2stratigraphy/slider_manager.py @@ -1,7 +1,7 @@ import matplotlib.pyplot as plt -import widgets -import utils +from . import widgets +from . import utils class SliderManager(object): def __init__(self, gui): diff --git a/rivers2stratigraphy/strat.py b/rivers2stratigraphy/strat.py index 9d8369b..865b819 100644 --- a/rivers2stratigraphy/strat.py +++ b/rivers2stratigraphy/strat.py @@ -6,8 +6,8 @@ import shapely.geometry as sg import shapely.ops as so -from channel import ActiveChannel, State, ChannelBody -import utils +from .channel import ActiveChannel, State, ChannelBody +from . import utils class Strat(object): From f1e2f9d26d490b6f94c24bca68cee398ec0a4509 Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 22:13:10 -0500 Subject: [PATCH 11/12] update version, aboutjson, script for SedEdu --- about.json | 2 +- run_rivers2stratigraphy.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/about.json b/about.json index 323fd2a..36e337b 100644 --- a/about.json +++ b/about.json @@ -1,7 +1,7 @@ { "title": "Rivers to Stratigraphy", "author": "Andrew J. Moodie", - "version": "0.2.0", + "version": "0.2.6", "shortdesc": "Explore how a migrating river becomes stratigraphy", "difficulty": 7, "license": "MIT", diff --git a/run_rivers2stratigraphy.py b/run_rivers2stratigraphy.py index 574a252..e3e81fe 100644 --- a/run_rivers2stratigraphy.py +++ b/run_rivers2stratigraphy.py @@ -1,9 +1,5 @@ import rivers2stratigraphy -# import os +print('launching activity...') -# ## initialize the GUI -# thisDir = os.path.dirname(__file__) -# thisPath = os.path.join(thisDir,'') -# execFile = os.path.join(thisPath, 'rivers2stratigraphy', 'rivers2stratigraphy.py') -# exec(open(execFile).read()) \ No newline at end of file +rivers2stratigraphy.run() From 224fdeccf791153c7225e0428db9840e34aa45de Mon Sep 17 00:00:00 2001 From: amoodie Date: Sun, 16 Sep 2018 22:23:54 -0500 Subject: [PATCH 12/12] update run instructions --- README.md | 10 ++++--- rivers2stratigraphy/gui.py | 7 ----- rivers2stratigraphy/launcher.py | 48 --------------------------------- 3 files changed, 6 insertions(+), 59 deletions(-) delete mode 100644 rivers2stratigraphy/launcher.py diff --git a/README.md b/README.md index 46afedd..e64ad21 100644 --- a/README.md +++ b/README.md @@ -79,16 +79,17 @@ Finally, run the module from the Python shell with: import rivers2stratigraphy ``` -You will be asked if you wish to launch the module now, type `Y` and hit enter. +Instructions will indicate to use the following command to then run the module: +``` +rivers2stratigraphy.run() +``` -Alternatively, run the module with provided script: +Alternatively, run the module with provided script (this is the hook used for launching from SedEdu): ``` python3 /run_rivers2stratigraphy.py ``` -Note that this may throw an error on closing the window, but this is not a problem to functionality. - Please [open an issue](https://github.com/amoodie/rivers2stratigraphy/issues) if you encounter any additional error messages! Please include 1) operating system, 2) installation method, and 3) copy-paste the error. @@ -102,6 +103,7 @@ If you are interested in contributing to code please see below for instructions. If you are interested in contributing to the the accompanying activites (which would be greatly appreciated!) please see [Writing Activites for SedEdu](https://github.com/amoodie/sededu/blob/develop/docs/writing_activities.md) + #### Download the source code You can download this entire repository as a `.zip` by clicking the "Clone or download button on this page", or by [clicking here](https://github.com/amoodie/rivers2stratigraphy/archive/master.zip) to get a `.zip` folder. Unzip the folder in your preferred location. diff --git a/rivers2stratigraphy/gui.py b/rivers2stratigraphy/gui.py index 049801b..3de468e 100644 --- a/rivers2stratigraphy/gui.py +++ b/rivers2stratigraphy/gui.py @@ -9,12 +9,6 @@ written by Andrew J. Moodie amoodie@rice.edu Feb 2018 - - TODO: - - control for "natural" ad default where lateral migration - and Ta are a function of sediment transport (Qw) - - """ @@ -24,7 +18,6 @@ import matplotlib.animation as animation from .strat import Strat -# import strat from .slider_manager import SliderManager from .channel import ActiveChannel, State, ChannelBody from . import geom, sedtrans, utils diff --git a/rivers2stratigraphy/launcher.py b/rivers2stratigraphy/launcher.py deleted file mode 100644 index ccedf2a..0000000 --- a/rivers2stratigraphy/launcher.py +++ /dev/null @@ -1,48 +0,0 @@ -import sys - -def get_response(): - - posInput = ['y', 'yes'] - negInput = ['n', 'no'] - allInput = posInput + negInput - - badInput = True - while badInput: - resp = input('\nWould you like to launch the module now? [y/n] \n').lower() - check = resp in allInput - badInput = not check - if badInput: - print('invalid input: select [y/n]') - - return resp - - -def check_response(resp): - if resp in ['y', 'yes']: - print('okay, launching module . . . . .\n') - return True - elif resp in ['n', 'no']: - print('okay, not launching module . . . . .\n') - return False - else: - raise ValueError('invalid input: select [y/n]') - - -def run_import(): - from . import main - - -def import_runner(): - ''' - a super simple method to run the gui from a Python shell - ''' - - print('\n SedEdu -- rivers2stratigraphy module') - - resp = get_response() - launch = check_response(resp) - if launch: - run_import() - else: - print('you will need to unload and reload module\n', - 'to run again, or relaunch python')