Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,46 @@ Everything is a plain [ipywidgets](https://ipywidgets.readthedocs.io/)/
[anywidget](https://anywidget.dev/) object: build one, call `.display()`, and
keep using its Python attributes from other cells.

### Import layers

| Import from | What it holds |
| --- | --- |
| `cadetgui` | the ready-made benches: `WorkbenchWidget`, `CharacterizationWorkbenchWidget` |
| `cadetgui.widgets.composite` | task widgets (configuration, simulation, measurements, steps, ...) |
| `cadetgui.widgets.elements` | atomic input controls and charts |
| `cadetgui.widgets` | the bench kit (`SidebarShell`, `bench_root`, `resolve_step`, `validate_steps`, `collect_panes`, `system_pane_label`, `SYSTEM_GROUP`) and HTML helpers (`status_html`, `term_html`, `info_box_html`, `checklist_html`, `style_tag`) |
| `cadetgui.characterization` | headless characterization core: `Study`, `Comparison`, `ParameterStore`, step setups and fits |
| `cadetgui.io` | file I/O: measured data, saved configurations, simulation runs |

### The workbench

`WorkbenchWidget` is the top-level entry point — a sidebar with four steps
(System Configuration, Process Configuration, Simulation, Parameter
Estimation), each also reachable as its own attribute: `.instrument`,
`.configuration`, `.solution`, `.parameter_estimation`.
`WorkbenchWidget` is a sidebar with four steps (System Configuration, Process
Configuration, Simulation, Parameter Estimation), each also reachable as its
own attribute: `.instrument`, `.configuration`, `.solution`,
`.parameter_estimation`.

```python
from cadetgui.widgets.composite import WorkbenchWidget
from cadetgui import WorkbenchWidget

workbench = WorkbenchWidget()
workbench.display()
```

See [`examples/workbench.ipynb`](examples/workbench.ipynb).

### The characterization workbench

`CharacterizationWorkbenchWidget` walks a study from the system and its
measurements through step-by-step parameter fits.

```python
from cadetgui import CharacterizationWorkbenchWidget

CharacterizationWorkbenchWidget().display()
```

See [`examples/characterization.ipynb`](examples/characterization.ipynb).

### Composing widgets yourself

Build and wire the same widgets by hand for a custom layout or a subset of
Expand All @@ -68,6 +92,9 @@ config.display()
solution.display()
```

To frame your own selection as a bench, hand `(label, widget)` panes to
`cadetgui.widgets.SidebarShell` and wrap it with `cadetgui.widgets.bench_root`.

See [`examples/configuration_and_solution.ipynb`](examples/configuration_and_solution.ipynb)
and [`examples/custom_sidebar.ipynb`](examples/custom_sidebar.ipynb).

Expand All @@ -80,8 +107,9 @@ files.

### Running as a web app

[`examples/webapp/`](examples/webapp/) deploys `WorkbenchWidget` as a
standalone page via [Voilà](https://voila.readthedocs.io/):
[`examples/webapp/`](examples/webapp/) deploys the instrument, configuration,
simulation and parameter-estimation widgets as a standalone tabbed page via
[Voilà](https://voila.readthedocs.io/):

```bash
pip install voila
Expand Down
20 changes: 19 additions & 1 deletion cadetgui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
"""Widget-based GUI layer on top of CADET-Process."""
"""Widget-based GUI layer on top of CADET-Process.

`WorkbenchWidget` and `CharacterizationWorkbenchWidget` are the ready-made benches;
they load lazily, so `import cadetgui` stays cheap.
"""
from typing import TYPE_CHECKING, Any

__version__ = "0.1.0"

if TYPE_CHECKING:
from .widgets.composite import CharacterizationWorkbenchWidget, WorkbenchWidget

__all__ = ["WorkbenchWidget", "CharacterizationWorkbenchWidget"]


def __getattr__(name: str) -> Any:
if name in __all__:
from .widgets import composite

return getattr(composite, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
44 changes: 33 additions & 11 deletions cadetgui/cadetprocessadapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,35 +84,38 @@ def parse_float_list(v: Any) -> list[float]:

CONCENTRATION_UNITS = r"\frac{\mathrm{mol}}{\mathrm{m}^{3}_{\mathrm{IV}}}"

# 1 mL/min in m^3/s, written at the 10 significant figures the event sliders round to.
DEFAULT_FLOW_RATE = 1.666666667e-8

# Shared scalar process fields (m^3/s, s). Per-component concentration fields are built
# by `_concentration_field` instead, since they depend on the component system.
PARAMS: dict[str, FieldSpec] = {
"flow_rate": FieldSpec(
"flow_rate", "float", "Flow rate", 1.0e-6,
"flow_rate", "float", "Flow rate", DEFAULT_FLOW_RATE,
validate=require_positive, units=r"\frac{\mathrm{m}^{3}}{\mathrm{s}}",
),
"flow_rate_wash": FieldSpec(
"flow_rate_wash", "float", "Flow rate", 1.0e-6,
"flow_rate_wash", "float", "Flow rate", DEFAULT_FLOW_RATE,
validate=require_positive, units=r"\frac{\mathrm{m}^{3}}{\mathrm{s}}",
),
"cycle_time": FieldSpec(
"cycle_time", "float", "Cycle time", 6000.0,
"cycle_time", "float", "Cycle time", 600.0,
validate=require_positive, units=r"\mathrm{s}",
),
"pulse_duration": FieldSpec(
"pulse_duration", "float", "Pulse duration", 60.0,
"pulse_duration", "float", "Pulse duration", 10.0,
validate=require_positive, units=r"\mathrm{s}",
),
"delta_t_wash": FieldSpec(
"delta_t_wash", "float", "Wash duration", 600.0,
"delta_t_wash", "float", "Wash duration", 300.0,
validate=require_positive, units=r"\mathrm{s}",
),
"delta_t_elute": FieldSpec(
"delta_t_elute", "float", "Elution duration", 1200.0,
"delta_t_elute", "float", "Elution duration", 900.0,
validate=require_positive, units=r"\mathrm{s}",
),
"delta_t_final_wash": FieldSpec(
"delta_t_final_wash", "float", "Final wash duration", 600.0,
"delta_t_final_wash", "float", "Final wash duration", 300.0,
validate=require_positive, units=r"\mathrm{s}",
),
}
Expand Down Expand Up @@ -406,6 +409,24 @@ def template_required_units(template: Callable[..., ModelSpec] | None) -> frozen
"column", "tubing_post_column", "tubing_detectors",
)

# Mixer/tubing units with their own parameter form and dead-volume seeding. The column's
# own form belongs to ConfigurationWidget; this set only covers the instrument's hardware.
CONFIGURABLE_UNITS: tuple[str, ...] = (
"mixer", "tubing_pre_injection", "tubing_pre_column", "tubing_post_column", "tubing_detectors",
)

# Non-degenerate starting values; CADET-Core has no canonical default for these.
_TUBING_SEED_DEFAULTS: Dict[str, float] = {
"diameter": 0.5e-3, "length": 0.1, "axial_dispersion": 1e-7,
}
UNIT_SEED_DEFAULTS: Dict[str, Dict[str, float]] = {
"mixer": {"init_liquid_volume": 1e-6},
"tubing_pre_injection": _TUBING_SEED_DEFAULTS,
"tubing_pre_column": _TUBING_SEED_DEFAULTS,
"tubing_post_column": _TUBING_SEED_DEFAULTS,
"tubing_detectors": _TUBING_SEED_DEFAULTS,
}

# Plain names for every LCFlowSheet unit.
UNIT_LABELS: dict[str, str] = {
"buffer_a": "Buffer A",
Expand Down Expand Up @@ -513,15 +534,16 @@ def _infer_kind(x: Any) -> str:
# Starting values so a blank form doesn't apply a degenerate column. A `None` model slot
# is shared across column models; a model name overrides it.
_GUI_SEED_DEFAULTS: dict[tuple[str, Optional[str], str], float] = {
("column", None, "diameter"): 0.024,
("column", None, "length"): 0.5,
("column", None, "diameter"): 0.005,
("column", None, "length"): 0.05,
("column", None, "axial_dispersion"): 1e-8,
("column", None, "bed_porosity"): 0.72,
("column", None, "total_porosity"): 0.72,
("column", None, "bed_porosity"): 0.37,
("column", None, "total_porosity"): 0.75,
("column", None, "particle_porosity"): 0.6,
("column", None, "particle_radius"): 5.0e-6,
("column", None, "film_diffusion"): 1e-3,
("column", None, "pore_diffusion"): 1e-10,
("column", "Cstr", "init_liquid_volume"): 1e-6,
}


Expand Down
40 changes: 40 additions & 0 deletions cadetgui/characterization/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Characterization core: studies, measurements, parameter stores and step fits.

Names below load lazily from their modules, so importing one module of this
package does not import the others.
"""
from importlib import import_module
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from .comparison import Comparison
from .parameter_store import ParameterStore
from .runner import StepResult, StepSetup
from .step_checks import StepStatus, study_steps_status
from .study import Study

_EXPORTS = {
"Study": "study",
"Comparison": "comparison",
"ParameterStore": "parameter_store",
"StepSetup": "runner",
"StepResult": "runner",
"StepStatus": "step_checks",
"study_steps_status": "step_checks",
}

__all__ = [
"Study",
"Comparison",
"ParameterStore",
"StepSetup",
"StepResult",
"StepStatus",
"study_steps_status",
]


def __getattr__(name: str) -> Any:
if name in _EXPORTS:
return getattr(import_module(f".{_EXPORTS[name]}", __name__), name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Loading
Loading