diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dada9105..4fe72062 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,10 +19,10 @@ jobs: # pure python wheels steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.config.py }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.config.py }} @@ -66,7 +66,7 @@ jobs: # See the following for how to upload to a release # https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: artifacts path: | @@ -80,7 +80,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Retrieve all artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: artifacts diff --git a/.github/workflows/unstable.yml b/.github/workflows/unstable.yml index 5f8ad970..302d985f 100644 --- a/.github/workflows/unstable.yml +++ b/.github/workflows/unstable.yml @@ -20,10 +20,10 @@ jobs: # all using to stable abi steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.config.py }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.config.py }} @@ -60,7 +60,7 @@ jobs: # See the following for how to upload to a release # https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ - name: Archive artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: artifacts path: | @@ -71,7 +71,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Retrieve all artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: artifacts diff --git a/.gitignore b/.gitignore index 7ff75743..292f3f53 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,13 @@ .idea *.pyd *.pyc +**/__pycache__/ +**/node_modules/ +/refl1d/webview/client/dist/ *.swp *.so *.bak +refl1d-webview-client*.tgz /build/ /dist/ /refl1d.egg-info/ diff --git a/MANIFEST.in b/MANIFEST.in index 2b910976..163692c4 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,6 +14,11 @@ include master_builder.py include refl1d.iss include setup_py2exe.py include refl1d/lib/*.h refl1d/lib/erf.c - +recursive-include refl1d/webview/client/dist *.html *.js *.css *.svg *.png +recursive-include refl1d/webview/client/src *.html *.js *.css *.svg *.png *.vue +include refl1d/webview/client/*.html +include refl1d/webview/client/*.js +include refl1d/webview/client/*.json +include refl1d/webview/client/*.txt # Delete files #prune this that diff --git a/extra/build_conda_packed.sh b/extra/build_conda_packed.sh new file mode 100755 index 00000000..28e2b7ef --- /dev/null +++ b/extra/build_conda_packed.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +ENV_NAME="isolated-base" +PYTHON_VERSION="3.10" +DIRNAME="refl1d" + +eval "$(conda shell.bash hook)" +conda activate base || { echo 'failed: conda not installed'; exit 1; } + +conda install -y conda-pack +conda create -n "$ENV_NAME" -y "python=$PYTHON_VERSION" +conda-pack -n "$ENV_NAME" -f -o "$ENV_NAME.tar.gz" + +# unpack the new environment, that contains only python + pip +tmpdir=$(mktemp -d) +destdir="$tmpdir/$DIRNAME" +mkdir "$destdir" +tar -xzf "$ENV_NAME.tar.gz" -C "$destdir" + +# activate the unpacked environment and install pip packages +conda deactivate +WORKING_DIRECTORY=$(pwd) +cd "$tmpdir" +source "$DIRNAME/bin/activate" +pip install --no-input numba +pip install --no-input git+https://github.com/bumps/bumps@dataclass_overlay +pip install --no-input git+https://github.com/reflectometry/refl1d@webview +pip install -r https://raw.githubusercontent.com/reflectometry/refl1d/webview/webview-requirements +source "$DIRNAME/bin/deactivate" + +# zip it back up +tar -czf "$WORKING_DIRECTORY/refl1d-webview-$(uname -s)-$(uname -m).tar.gz" "$DIRNAME" diff --git a/refl1d/errors.py b/refl1d/errors.py index 431125cc..c0d03d78 100644 --- a/refl1d/errors.py +++ b/refl1d/errors.py @@ -161,7 +161,7 @@ def calc_errors(problem, points): # TODO: return sane datastructure # Make a hashable version of model which just contains the name # attribute, which is all that the rest of this code accesses. - models = [_HashableModel(m) for m in _experiments(problem)] + models = [_HashableModel(m, i) for i, m in enumerate(_experiments(problem))] profiles = {h: [v[k] for v in profiles] for k, h in enumerate(models)} slabs = {h: [v[k] for v in slabs] for k, h in enumerate(models)} @@ -179,8 +179,8 @@ def calc_errors(problem, points): class _HashableModel: name: str - def __init__(self, model): - self.name = model.name + def __init__(self, model, index): + self.name = model.name if model.name is not None else f"M{index}" def __str__(self): return f"model {self.name}" @@ -229,7 +229,7 @@ def align_profiles(profiles, slabs, align): for m in profiles.keys()) def show_errors(errors, contours=CONTOURS, npoints=200, - align='auto', plots=1, save=None): + align='auto', plots=1, save=None, fig=None): """ Plot the aligned profiles and the distribution of the residuals for profiles and residuals returned from calc_errors. @@ -259,15 +259,20 @@ def show_errors(errors, contours=CONTOURS, npoints=200, """ import matplotlib.pyplot as plt + if fig is not None and plots != 1: + raise ValueError("can only pass in a figure object if exactly 1 plot is requested") + if plots == 0: # Don't create plots, just save the data _save_profile_data(errors, contours=contours, npoints=npoints, align=align, save=save) _save_residual_data(errors, contours=contours, save=save) elif plots == 1: # Subplots for profiles/residuals - plt.subplot(211) - show_profiles(errors, contours=contours, npoints=npoints, align=align) - plt.subplot(212) - show_residuals(errors, contours=contours) + if fig is None: + fig = plt.gcf() + ax_profiles = fig.add_subplot(211) + show_profiles(errors, contours=contours, npoints=npoints, align=align, axes=ax_profiles) + ax_residuals = fig.add_subplot(212) + show_residuals(errors, contours=contours, axes=ax_residuals) if save: plt.savefig(save+"-err.png") elif plots == 2: # Separate plots for profiles/residuals @@ -296,24 +301,24 @@ def show_errors(errors, contours=CONTOURS, npoints=200, plt.savefig(save+"-err%d.png"%fignum) fignum += 1 -def show_profiles(errors, align, contours, npoints): +def show_profiles(errors, align, contours, npoints, axes=None): profiles, slabs, _, _ = errors if align is not None: profiles = align_profiles(profiles, slabs, align) if contours: - _profiles_contour(profiles, contours, npoints) + _profiles_contour(profiles, contours, npoints, axes=axes) else: - _profiles_overplot(profiles) + _profiles_overplot(profiles, axes=axes) -def show_residuals(errors, contours): +def show_residuals(errors, contours, axes=None): _, _, Q, residuals = errors if False and contours: _residuals_contour(Q, residuals, contours=contours) else: - _residuals_overplot(Q, residuals) + _residuals_overplot(Q, residuals, axes=axes) def _save_profile_data(errors, align, contours, npoints, save): @@ -376,34 +381,36 @@ def _write_file(path, data, title, columns): def dark(color): return dhsv(color, dv=-0.2) -def _profiles_overplot(profiles): +def _profiles_overplot(profiles, axes=None): for model, group in profiles.items(): name = model.name absorbing = any((L[2] != 1e-4).any() for L in group) magnetic = (len(group[0]) > 3) # Note: Use 3 colours per dataset for consistency - _draw_overplot(group, 1, name + ' rho') + _draw_overplot(group, 1, name + ' rho', axes=axes) if absorbing: - _draw_overplot(group, 2, name + ' irho') + _draw_overplot(group, 2, name + ' irho', axes=axes) else: - next_color() + next_color(axes=axes) if magnetic: - _draw_overplot(group, 3, name + ' rhoM') + _draw_overplot(group, 3, name + ' rhoM', axes=axes) else: - next_color() - _profile_labels() + next_color(axes=axes) + _profile_labels(axes=axes) -def _draw_overplot(group, index, label): +def _draw_overplot(group, index, label, axes=None): import matplotlib.pyplot as plt + if axes is None: + axes = plt.gca() alpha = 0.1 - color = next_color() + color = next_color(axes=axes) for L in group[1:]: - plt.plot(L[0], L[index], '-', color=color, alpha=alpha) + axes.plot(L[0], L[index], '-', color=color, alpha=alpha) # Plot best L = group[0] - plt.plot(L[0], L[index], '-', label=label, color=dark(color)) + axes.plot(L[0], L[index], '-', label=label, color=dark(color)) -def _profiles_contour(profiles, contours=CONTOURS, npoints=200): +def _profiles_contour(profiles, contours=CONTOURS, npoints=200, axes=None): for model, group in profiles.items(): name = model.name if model.name is not None else 'model' absorbing = any((L[2] > 1e-4).any() for L in group) @@ -412,46 +419,52 @@ def _profiles_contour(profiles, contours=CONTOURS, npoints=200): z = np.hstack([line[0] for line in group]) zp = np.linspace(np.min(z), np.max(z), npoints) # Note: Use 3 colours per dataset for consistency - _draw_contours(group, 1, name + ' rho', zp, contours) + _draw_contours(group, 1, name + ' rho', zp, contours, axes=axes) if absorbing: - _draw_contours(group, 2, name + ' irho', zp, contours) + _draw_contours(group, 2, name + ' irho', zp, contours, axes=axes) else: - next_color() + next_color(axes=axes) if magnetic: - _draw_contours(group, 3, name + ' rhoM', zp, contours) + _draw_contours(group, 3, name + ' rhoM', zp, contours, axes=axes) else: - next_color() - _profile_labels() + next_color(axes=axes) + _profile_labels(axes=axes) -def _draw_contours(group, index, label, zp, contours): +def _draw_contours(group, index, label, zp, contours, axes=None): import matplotlib.pyplot as plt - color = next_color() + if axes is None: + axes = plt.gca() + color = next_color(axes=axes) # Interpolate on common z fp = np.vstack([np.interp(zp, L[0], L[index]) for L in group]) # Plot the quantiles - plot_quantiles(zp, fp, contours, color) + plot_quantiles(zp, fp, contours, color, axes=axes) # Plot the best - plt.plot(zp, fp[0], '-', label=label, color=dark(color)) + axes.plot(zp, fp[0], '-', label=label, color=dark(color)) -def _profile_labels(): +def _profile_labels(axes=None): import matplotlib.pyplot as plt - plt.legend() - plt.xlabel(u'z (Å)') - plt.ylabel(u'SLD (10⁻⁶/Ų)') + if axes is None: + axes = plt.gca() + axes.legend() + axes.set_xlabel(u'z (Å)') + axes.set_ylabel(u'SLD (10⁻⁶/Ų)') -def _residuals_overplot(Q, residuals): +def _residuals_overplot(Q, residuals, axes=None): import matplotlib.pyplot as plt + if axes is None: + axes = plt.gca() alpha = 0.4 shift = 0 for m, r in residuals.items(): - color = next_color() - plt.plot(Q[m], shift+r[:, 1:], '.', markersize=1, color=color, alpha=alpha) - plt.plot(Q[m], shift+r[:, 0], '.', label=m.name, markersize=1, color=dark(color)) + color = next_color(axes=axes) + axes.plot(Q[m], shift+r[:, 1:], '.', markersize=1, color=color, alpha=alpha) + axes.plot(Q[m], shift+r[:, 0], '.', label=m.name, markersize=1, color=dark(color)) # Use 3 colours from cycle so reflectivity matches rho for each dataset - next_color() - next_color() + next_color(axes=axes) + next_color(axes=axes) shift += 5 - _residuals_labels() + _residuals_labels(axes=axes) def _residuals_contour(Q, residuals, contours=CONTOURS): import matplotlib.pyplot as plt @@ -466,11 +479,13 @@ def _residuals_contour(Q, residuals, contours=CONTOURS): shift += 5 _residuals_labels() -def _residuals_labels(): +def _residuals_labels(axes=None): import matplotlib.pyplot as plt - plt.legend() - plt.xlabel(u'Q (1/Å)') - plt.ylabel(u'Residuals') + if axes is None: + axes = plt.gca() + axes.legend() + axes.set_xlabel(u'Q (1/Å)') + axes.set_ylabel(u'Residuals') # ==== Helper functions ===== diff --git a/refl1d/experiment.py b/refl1d/experiment.py index a2944f50..c1f0e687 100644 --- a/refl1d/experiment.py +++ b/refl1d/experiment.py @@ -677,7 +677,7 @@ def __init__(self, samples=None, ratio=None, probe=None, self.probe = probe self.ratio = [Parameter.default(r, name="ratio %d"%i) for i, r in enumerate(ratio)] - self.parts = [Experiment(s, probe, **kw) for s in samples] + self.parts = [Experiment(s, probe, name=s.name, **kw) for s in samples] self.coherent = coherent self.interpolation = interpolation self._substrate = self.samples[0][0].material diff --git a/refl1d/material.py b/refl1d/material.py index 92434be6..02188dd4 100644 --- a/refl1d/material.py +++ b/refl1d/material.py @@ -169,177 +169,254 @@ def sld(self, probe): # ELEMENTS = Enum('elements', [(e.name, e.symbol) for e in periodictable.elements._element.values()]) @schema() -class Material(Scatterer): +class BaseMaterial(Scatterer): """ Description of a solid block of material. :Parameters: + *name* : string + *formula* : Formula Composition can be initialized from either a string or a chemical formula. Valid values are defined in periodictable.formula. - *density* : float | |g/cm^3| + *use_incoherent* = False : boolean - If specified, set the bulk density for the material. + True if incoherent scattering should be interpreted as absorption. + + """ + name: str + formula: str # Formula + use_incoherent: bool = False + density: Union[Parameter, Expression] # to be filled in by inheriting classes - *natural_density* : float | |g/cm^3| + # not in the schema: + _formula: BaseFormula - If specified, set the natural bulk density for the material. + def __init__(self, formula: Union[str, BaseFormula], density=None, natural_density=None, name=None, use_incoherent=False): + # coerce formula to string repr: + self.formula = str(formula) + self._formula: BaseFormula = periodictable.formula(formula, density=density, natural_density=natural_density) + self.name = name if name is not None else str(self._formula) + self.use_incoherent = use_incoherent - *use_incoherent* = False : boolean + def sld(self, probe): + rho, irho, incoh = probe.scattering_factors( + self._formula, density=self.density.value) + if self.use_incoherent: + raise NotImplementedError("incoherent scattering not supported") + #irho += incoh + return rho, irho + def __str__(self): + return self.name + def __repr__(self): + return "Material(%s)"%self.name - True if incoherent scattering should be interpreted as absorption. - *fitby* = 'bulk_density' : string +FitByChoices = Literal["bulk_density", "natural_density", "relative_density", "number_density", "cell_volume"] + +def Material( + formula: Union[str, BaseFormula], + density: Optional[float]=None, + natural_density: Optional[float]=None, + name: Optional[str]=None, + use_incoherent: bool=False, + fitby: FitByChoices="bulk_density", + **kw): + if fitby == "bulk_density": + return BulkDensityMaterial(formula, density=density, name=name, use_incoherent=use_incoherent) + elif fitby == "natural_density": + return NaturalDensityMaterial(formula, natural_density=natural_density, name=name, use_incoherent=use_incoherent) + elif fitby == "relative_density": + return RelativeDensityMaterial(formula, density=density, natural_density=natural_density, name=name, use_incoherent=use_incoherent, **kw) + elif fitby == "number_density": + return NumberDensityMaterial(formula, density=density, natural_density=natural_density, name=name, use_incoherent=use_incoherent, **kw) + elif fitby == "cell_volume": + return CellVolumeMaterial(formula, density=density, natural_density=natural_density, name=name, use_incoherent=use_incoherent, **kw) + else: + raise ValueError(f'unknown value "{fitby}" of fitby: should be one of ["bulk_density", "natural_density", "relative_density", "number_density", "cell_volume"]') - Which density parameter is the fitting parameter. The choices - are *bulk_density*, *natural_density*, *relative_density* or - *cell_volume*. See :meth:`fitby` for details. - *value* : Parameter or float | units depends on fitby type +@schema() +class BulkDensityMaterial(BaseMaterial): + """ + A solid block of material, described by its bulk density - Initial value for the fitted density parameter. If None, the - value will be initialized from the material density. + :Parameters: + *density* : float | |g/cm^3| + the bulk density for the material. + """ + bulk_density: Parameter + density: Parameter + + def __init__(self, + formula: Union[str, BaseFormula], + density: Optional[Union[float, Parameter]]=None, + name=None, + use_incoherent=False): + BaseMaterial.__init__(self, formula, density=density, name=name, use_incoherent=use_incoherent) + if density is None: + if self._formula.density is not None: + density = self._formula.density + else: + raise ValueError(f"material {self._formula} does not have known density: please provide it in arguments") + self.density = Parameter.default(density, name=self.name+" density", limits=(0, inf)) + self.bulk_density = self.density - For example, to fit Pd by cell volume use:: + def parameters(self): + return dict(density=self.density) - >>> m = Material('Pd', fitby='cell_volume') - >>> m.cell_volume.range(1, 10) - Parameter(Pd cell volume) - >>> print("%.2f %.2f"%(m.density.value, m.cell_volume.value)) - 12.02 14.70 - You can change density representation by calling *material.fitby(type)*. +@schema() +class NaturalDensityMaterial(BaseMaterial): + """ + A solid block of material, described by its natural density + :Parameters: + *natural_density* : float | |g/cm^3| + the natural bulk density for the material. """ - name: str - formula: str # Formula - formula_density: float - formula_natural_density: Union[float, Literal[None]] - # density: Parameter - value: Parameter - fitby: Literal['bulk_density', 'number_density', 'natural_density', 'relative_density', 'cell_volume'] = 'bulk_density' - use_incoherent: bool = False + natural_density: Parameter + density: Expression + + def __init__(self, + formula: Union[str, BaseFormula], + natural_density: Optional[Union[float, Parameter]]=None, + name=None, + use_incoherent=False): + BaseMaterial.__init__(self, formula, natural_density=natural_density, name=name, use_incoherent=use_incoherent) + if natural_density is None: + if self._formula.density is not None: + natural_density = self._formula.density + else: + raise ValueError(f"material {self._formula} does not have known natural_density: please provide it in arguments") + self.natural_density = Parameter.default(natural_density, name=self.name+" nat. density", limits=(0, inf)) + self.density = self.natural_density / self._formula.natural_mass_ratio() - def __init__(self, formula=None, name=None, use_incoherent=False, - formula_density=None, formula_natural_density=None, - fitby='bulk_density', value=None): - self._formula = periodictable.formula(formula, density=formula_density, - natural_density=formula_natural_density) - self.formula_density = formula_density - self.formula_natural_density = formula_natural_density - self.formula = formula - self.name = name if name is not None else str(self._formula) - self.density = Parameter(name=self.name + " density") + def parameters(self): + return dict(density=self.density, natural_density=self.natural_density) - self.use_incoherent = use_incoherent - self._fitby(type=fitby, value=value) - def _fitby(self, type='bulk_density', value=None): - """ - Specify the fitting parameter to use for material density. - - :Parameters: - *type* : string - Density representation - *value* : Parameter - Initial value, or associated parameter. - - Density type can be one of the following: - - *bulk_density* : |g/cm^3| or kg/L - Density is *bulk_density* - *natural_density* : |g/cm^3| or kg/L - Density is *natural_density* / (natural mass/isotope mass) - *relative_density* : unitless - Density is *relative_density* * formula density - *cell_volume* : |Ang^3| - Density is mass / *cell_volume* - *number_density*: [atoms/cm^3] +@schema() +class NumberDensityMaterial(BaseMaterial): + """ + A solid block of material, described by its number density + + :Parameters: + *number_density*: [atoms/cm^3] Density is *number_density* * molar mass / avogadro constant - The resulting material will have a *density* attribute with the - computed material density in addition to the *fitby* - attribute specified. + *density* : float | |g/cm^3| + if specified, the bulk density for the material. - .. Note:: + *natural_density* : float | |g/cm^3| + if specified, the natural bulk density for the material. + """ + number_density: Parameter + density: Expression + + def __init__(self, + formula: Union[str, BaseFormula], + number_density: Optional[Union[float, Parameter]]=None, + density: Optional[float]=None, + natural_density: Optional[float]=None, + name=None, + use_incoherent=False): + BaseMaterial.__init__(self, formula, density=density, natural_density=natural_density, name=name, use_incoherent=use_incoherent) + if number_density is None: + if self._formula.density is not None: + number_density = avogadro_number / self._formula.mass * self._formula.density + else: + raise ValueError(f"material {self._formula} does not have known density: please provide density or natural_density argument") + self.number_density = Parameter.default(number_density, name=self.name+" number density", limits=(0, inf)) + self.density = self.number_density / avogadro_number * self._formula.mass + self._formula.density = float(self.density) - Calling *fitby* replaces the *density* parameter in the - material, so be sure to do so before using *density* in a - parameter expression. Using *bumps.parameter.WrappedParameter* - for *density* is another alternative. - """ + def parameters(self): + return dict(density=self.density, number_density=self.number_density) - if type == 'bulk_density': - if value is None: - value = self._formula.density - self.value = Parameter.default( - value, name=self.name+" density", limits=(0, None)) - self.density.equals(self.value) - elif type == "number_density": - if value is None: - value = avogadro_number / self._formula.mass * self._formula.density - self.value = Parameter.default( - value, name=self.name+" number density", limits=(0, None)) - self.density.equals(self.value / avogadro_number * self._formula.mass) - elif type == 'natural_density': - if value is None: - value = self._formula.natural_density - self.value = Parameter.default( - value, name=self.name+" nat. density", limits=(0, None)) - self.density.equals(self.value / self._formula.natural_mass_ratio()) - elif type == 'relative_density': - if value is None: - value = 1 - self.value = Parameter.default( - value, name=self.name+" rel. density", limits=(0, None)) - self.density.equals(self._formula.density*self.value) - ## packing factor code should be correct, but radii are unreliable - #elif type is 'packing_factor': - # max_density = self.formula.mass/self.formula.volume(packing_factor=1) - # if value is None: - # value = self.formula.density/max_density - # self.packing_factor = Parameter.default( - # value, name=self.name+" packing factor") - # self.density = self.packing_factor * max_density - elif type == 'cell_volume': - # Density is in grams/cm^3. - # Mass is in grams. - # Volume is in A^3 = 1e24*cm^3. - if value is None: - value = (1e24*self._formula.molecular_mass)/self._formula.density - self.value = Parameter.default( - value, name=self.name+" cell volume", limits=(0, None)) - self.density.equals((1e24*self._formula.molecular_mass)/self.value) - else: - raise ValueError("Unknown density calculation type '%s'"%type) - self.fitby = type + +@schema() +class RelativeDensityMaterial(BaseMaterial): + """ + A solid block of material, described by its relative density + + :Parameters: + *relative_density* : unitless + Density is *relative_density* * formula density + + *density* : float | |g/cm^3| + if specified, the bulk density for the material. + + *natural_density* : float | |g/cm^3| + if specified, the natural bulk density for the material. + """ + relative_density: Parameter + + def __init__(self, + formula: Union[str, BaseFormula], + relative_density: Optional[Union[float, Parameter]]=None, + density: Optional[float]=None, + natural_density: Optional[float]=None, + name=None, + use_incoherent=False): + BaseMaterial.__init__(self, formula, density=density, natural_density=natural_density, name=name, use_incoherent=use_incoherent) + if self._formula.density is None and density is None and natural_density is None: + raise ValueError(f"material {self._formula} does not have known density: please provide density or natural_density argument") + if relative_density is None: + relative_density = 1 + self.relative_density = Parameter.default(relative_density, name=self.name+" rel. density", limits=(0, inf)) + self.density = self._formula.density*self.relative_density def parameters(self): - return {'density': self.density, "value": self.value} + return dict(density=self.density, relative_density=self.relative_density) - def to_dict(self): - return to_dict({ - 'type': type(self).__name__, - 'name': self.name, - 'formula': self.formula, - 'density': self.density, - 'use_incoherent': self.use_incoherent, - # TODO: what about fitby, natural_density and cell_volume? - }) - def sld(self, probe): - rho, irho, incoh = probe.scattering_factors( - self._formula, density=self.density.value) - if self.use_incoherent: - raise NotImplementedError("incoherent scattering not supported") - #irho += incoh - return rho, irho - def __str__(self): - return self.name - def __repr__(self): - return "Material(%s)"%self.name +@schema() +class CellVolumeMaterial(BaseMaterial): + """ + A solid block of material, described by the volume of one unit cell + + :Parameters: + *cell_volume* : |Ang^3| + Density is mass / *cell_volume* + + *density* : float | |g/cm^3| + if specified, the bulk density for the material. + + *natural_density* : float | |g/cm^3| + if specified, the natural bulk density for the material. + + + For example, to fit Pd by cell volume use:: + >>> m = Material('Pd', fitby='cell_volume') + >>> m.cell_volume.range(1, 10) + Parameter(Pd cell volume) + >>> print("%.2f %.2f"%(m.density.value, m.cell_volume.value)) + 12.02 14.70 + + """ + cell_volume: Parameter + + def __init__(self, + formula: Union[str, BaseFormula], + cell_volume: Optional[Union[float, Parameter]]=None, + density: Optional[float]=None, + natural_density: Optional[float]=None, + name=None, + use_incoherent=False): + BaseMaterial.__init__(self, formula, density=density, natural_density=natural_density, name=name, use_incoherent=use_incoherent) + if self._formula.density is None and density is None and natural_density is None: + raise ValueError(f"material {self._formula} does not have known density: please provide density or natural_density argument") + if cell_volume is None: + cell_volume = (1e24*self._formula.molecular_mass)/self._formula.density + self.cell_volume = Parameter.default(cell_volume, name=self.name+" cell volume", limits=(0, inf)) + self.density = (1e24*self._formula.molecular_mass)/self.cell_volume + + def parameters(self): + return dict(density=self.density, cell_volume=self.cell_volume) + class Compound(Scatterer): """ diff --git a/refl1d/materialdb.py b/refl1d/materialdb.py index 465c15ed..60bdffec 100644 --- a/refl1d/materialdb.py +++ b/refl1d/materialdb.py @@ -32,10 +32,10 @@ rho_DHO = periodictable.formula('DHO').mass/periodictable.formula('H2O').mass air = Vacuum() -water = H2O = Material('H2O', formula_density=1, name='water') -heavywater = D2O = Material('D2O', formula_density=rho_D2O) -lightheavywater = DHO = Material('DHO', formula_density=rho_DHO) +water = H2O = Material('H2O', density=1, name='water') +heavywater = D2O = Material('D2O', density=rho_D2O) +lightheavywater = DHO = Material('DHO', density=rho_DHO) silicon = Si = Material('Si') -sapphire = Al2O3 = Material('Al2O3', formula_density=3.965, name='sapphire') +sapphire = Al2O3 = Material('Al2O3', density=3.965, name='sapphire') gold = Au = Material('Au', name='gold') -permalloy = Ni8Fe2 = Material('Ni8Fe2', formula_density=8.692, name='permalloy') +permalloy = Ni8Fe2 = Material('Ni8Fe2', density=8.692, name='permalloy') diff --git a/refl1d/probe.py b/refl1d/probe.py index 9c10f09e..a293f0f2 100644 --- a/refl1d/probe.py +++ b/refl1d/probe.py @@ -257,6 +257,17 @@ def _set_TLR(self, T, dT, L, dL, R, dR, dQ): # Make sure that we are dealing with vectors T, dT, L, dL = [np.ones_like(Q)*v for v in (T, dT, L, dL)] + # remove nan + nan_indices = set() + for column in [T, dT, L, dL, R, dR, Q, dQ]: + if column is not None: + indices = np.argwhere(np.isnan(column)).flatten() + nan_indices.update(indices) + + nan_indices = list(nan_indices) + T, dT, L, dL, R, dR, Q, dQ = ( + np.delete(c, nan_indices) if c is not None else None for c in [T, dT, L, dL, R, dR, Q, dQ]) + # Probe stores sorted values for convenience of resolution calculator idx = np.argsort(Q) self.T, self.dT = T[idx], dT[idx] @@ -1562,7 +1573,7 @@ def fetch_key(key, override): T=data_T, dT=data_dT, L=data_L, dL=data_dL, data=(data_R, data_dR), - dQ=data_dQ, + dQo=data_dQ, **probe_args) else: # QProbe doesn't accept theta_offset or sample_broadening diff --git a/refl1d/webview/__init__.py b/refl1d/webview/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/refl1d/webview/client/.npmignore b/refl1d/webview/client/.npmignore new file mode 100644 index 00000000..e2d91bfc --- /dev/null +++ b/refl1d/webview/client/.npmignore @@ -0,0 +1 @@ +refl1d-webview-client*.tgz diff --git a/refl1d/webview/client/README.md b/refl1d/webview/client/README.md new file mode 100644 index 00000000..df525e22 --- /dev/null +++ b/refl1d/webview/client/README.md @@ -0,0 +1,43 @@ +# refl1d-web-gui + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Compile and Minify for Production + +```sh +npm run build +``` + +# Publishing new client versions: +(...after checking to make sure there aren't extraneous files in this folder) +```sh +npm version patch +npm publish +``` + +and then +```sh +git commit package.json -m "webview client version bump" +git pull +git push +``` \ No newline at end of file diff --git a/refl1d/webview/client/index.html b/refl1d/webview/client/index.html new file mode 100644 index 00000000..c55b2ea8 --- /dev/null +++ b/refl1d/webview/client/index.html @@ -0,0 +1,16 @@ + + +
+ + + + +