diff --git a/sirepo/feature_config.py b/sirepo/feature_config.py
index d424c8e4e..36cd39bb9 100644
--- a/sirepo/feature_config.py
+++ b/sirepo/feature_config.py
@@ -36,7 +36,8 @@ ALPHA_CODES = frozenset((
 ALL_CODES = NON_ALPHA_CODES.union(ALPHA_CODES)
 
 
-_DEFAULT_PROPRIETARY_CODES = ('flash',)
+#: codes which we include in dev for testing
+_DEV_PROPRIETARY_CODES = ('flash',)
 
 
 #: Configuration
@@ -76,32 +77,19 @@ def _init():
     from pykern import pkconfig
     global _cfg
 
-    @pkconfig.parse_none
-    def _cfg_sim_types(value):
-        res = pkconfig.parse_set(value)
-        if not res:
-            return tuple(_codes())
-        for c in res:
-            assert c in _codes(), \
-                'invalid sim_type={}, expected one of={}'.format(c, _codes())
-        if 'jspec' in res:
-            res = set(res)
-            res.add('elegant')
-        return tuple(res)
-
-    def _codes():
-        return ALL_CODES if pkconfig.channel_in_internal_test() \
-            else NON_ALPHA_CODES
-
     _cfg = pkconfig.init(
         api_modules=((), set, 'optional api modules, e.g. status'),
         jspec=dict(
             derbenevskrinsky_force_formula=(pkconfig.channel_in_internal_test(), bool, 'Include Derbenev-Skrinsky force forumla'),
         ),
-        proprietary_sim_types=(_DEFAULT_PROPRIETARY_CODES, set, 'codes that require authorization'),
+        proprietary_sim_types=(
+            _DEV_PROPRIETARY_CODES if pkconfig.channel_in('dev') else set(),
+            set,
+            'codes that require authorization',
+        ),
         #TODO(robnagler) make sim_type config
         rs4pi_dose_calc=(False, bool, 'run the real dose calculator'),
-        sim_types=(None, _cfg_sim_types, 'simulation types (codes) to be imported'),
+        sim_types=(set(), set, 'simulation types (codes) to be imported'),
         srw=dict(
             mask_in_toolbar=(pkconfig.channel_in_internal_test(), bool, 'Show the mask element in toolbar'),
             beamline3d=(pkconfig.channel_in_internal_test(), bool, 'Show 3D beamline plot'),
@@ -111,4 +99,16 @@ def _init():
             display_test_boxes=(pkconfig.channel_in_internal_test(), bool, 'Display test boxes to visualize 3D -> 2D projections'),
         ),
     )
+    a = ALL_CODES if pkconfig.channel_in_internal_test() else NON_ALPHA_CODES
+    s = set(_cfg.sim_types) if _cfg.sim_types else set(a)
+    s.update(_cfg.proprietary_sim_types)
+    # jspec imports elegant, but elegant won't work if it is not a valid
+    # sim_type so need to include here. Need a better model of
+    # dependencies between codes.
+    if 'jspec' in s and 'elegant' not in s:
+        s.add('elegant')
+    x = s.difference(a)
+    assert not x, \
+        'sim_type(s) invalid={} expected={}'.format(x, a)
+    _cfg.sim_types = frozenset(s)
     return _cfg
diff --git a/sirepo/pkcli/admin.py b/sirepo/pkcli/admin.py
index cc3ba3705..243d5098b 100644
--- a/sirepo/pkcli/admin.py
+++ b/sirepo/pkcli/admin.py
@@ -64,7 +64,7 @@ def audit_proprietary_lib_files(*uid):
                 pass
 
     server.init()
-    t = feature_config.cfg().proprietary_sim_types.intersection(feature_config.cfg().sim_types)
+    t = feature_config.cfg().proprietary_sim_types
     if not t:
         return
     for u in uid or auth_db.all_uids():
@@ -99,13 +99,12 @@ def setup_dev_proprietary_code(sim_type, rpm_url):
       sim_type (str): simulation type
       rpm_url (str): Url of the rpm (file:// or http://)
     """
-    import sirepo.pkcli.admin
     import urllib.request
 
     assert pkconfig.channel_in('dev'), \
         'Only to be used in dev. channel={}'.format(pkconfig.cfg.channel)
 
-    d = sirepo.pkcli.admin.proprietary_code_dir(sim_type)
+    d = proprietary_code_dir(sim_type)
     pkio.mkdir_parent(d)
     s = sirepo.sim_data.get_class(sim_type)
 
diff --git a/sirepo/simulation_db.py b/sirepo/simulation_db.py
index 639f50432..963120045 100644
--- a/sirepo/simulation_db.py
+++ b/sirepo/simulation_db.py
@@ -254,7 +254,7 @@ def get_schema(sim_type):
 
     """
     t = sirepo.template.assert_sim_type(sim_type) if sim_type is not None \
-        else feature_config.cfg().sim_types[0]
+        else list(feature_config.cfg().sim_types)[0]
     if t in _SCHEMA_CACHE:
         return _SCHEMA_CACHE[t]
     schema = read_json(

add values from proprietary_sim_types to sim_types
proprietary_sim_types should be unioned with sim_types so that any proprietary_sim_types is guaranteed in sim_types.  This makes configuration management easier.

default proprietary sim types can include flash for dev, but not alpha, etc.


diff --git a/tests/feature_config_test.py b/tests/feature_config_test.py
new file mode 100644
index 000000000..af1d26de0
--- /dev/null
+++ b/tests/feature_config_test.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+u"""test sim_types
+
+:copyright: Copyright (c) 2020 RadiaSoft LLC.  All Rights Reserved.
+:license: http://www.apache.org/licenses/LICENSE-2.0.html
+"""
+from pykern.pkcollections import PKDict
+import os
+import pytest
+
+
+def setup_module(module):
+    os.environ.update(
+        SIREPO_FEATURE_CONFIG_PROPRIETARY_SIM_TYPES='xyz1',
+        SIREPO_FEATURE_CONFIG_SIM_TYPES='xyz2',
+    )
+
+
+def test_1():
+    from pykern import pkunit
+    import sirepo.feature_config
+
+    with pkunit.pkexcept('xyz.*xyz'):
+        sirepo.feature_config.cfg()

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc nodejs npm
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aenum==3.1.15
alabaster==0.7.16
argh==0.31.3
asyncssh==2.20.0
Authlib==1.5.1
babel==2.17.0
black==24.10.0
blinker==1.9.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
cryptography==44.0.2
dnspython==2.7.0
docutils==0.21.2
et_xmlfile==2.0.0
exceptiongroup==1.2.2
Flask==3.1.0
Flask-Mail==0.10.0
future==1.0.0
futures==3.0.5
github3.py==4.0.1
greenlet==3.1.1
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
msgpack==1.1.0
mypy-extensions==1.0.0
numconv==2.1.1
numpy==2.0.2
openpyxl==3.1.5
packaging==24.2
pandas==2.2.3
path==17.1.0
path.py==12.5.0
pathspec==0.12.1
platformdirs==4.3.7
pluggy==1.5.0
psutil==7.0.0
py==1.11.0
py-cpuinfo==9.0.0
pycparser==2.22
Pygments==2.19.1
pyIsEmail==2.0.1
PyJWT==2.10.1
pykern==20250224.223823
pytest==8.3.5
pytest-asyncio==0.26.0
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.32.3
ruamel.yaml==0.18.10
ruamel.yaml.clib==0.2.12
-e git+https://github.com/radiasoft/sirepo.git@0776df7f46192b320056f5066368a943d2d08aa4#egg=sirepo
six==1.17.0
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
SQLAlchemy==2.0.40
toml==0.10.2
tomli==2.2.1
tornado==6.4.2
typing_extensions==4.13.0
tzdata==2025.2
ua-parser==1.0.1
ua-parser-builtins==0.18.0.post1
uritemplate==4.1.1
urllib3==2.3.0
user-agents==2.2.0
uWSGI==2.0.28
Werkzeug==3.1.3
XlsxWriter==3.2.2
zipp==3.21.0

name: sirepo
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aenum==3.1.15
      - alabaster==0.7.16
      - argh==0.31.3
      - asyncssh==2.20.0
      - authlib==1.5.1
      - babel==2.17.0
      - black==24.10.0
      - blinker==1.9.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - click==8.1.8
      - cryptography==44.0.2
      - dnspython==2.7.0
      - docutils==0.21.2
      - et-xmlfile==2.0.0
      - exceptiongroup==1.2.2
      - flask==3.1.0
      - flask-mail==0.10.0
      - future==1.0.0
      - futures==3.0.5
      - github3-py==4.0.1
      - greenlet==3.1.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - itsdangerous==2.2.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - msgpack==1.1.0
      - mypy-extensions==1.0.0
      - numconv==2.1.1
      - numpy==2.0.2
      - openpyxl==3.1.5
      - packaging==24.2
      - pandas==2.2.3
      - path==17.1.0
      - path-py==12.5.0
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - psutil==7.0.0
      - py==1.11.0
      - py-cpuinfo==9.0.0
      - pycparser==2.22
      - pygments==2.19.1
      - pyisemail==2.0.1
      - pyjwt==2.10.1
      - pykern==20250224.223823
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.32.3
      - ruamel-yaml==0.18.10
      - ruamel-yaml-clib==0.2.12
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - sqlalchemy==2.0.40
      - toml==0.10.2
      - tomli==2.2.1
      - tornado==6.4.2
      - typing-extensions==4.13.0
      - tzdata==2025.2
      - ua-parser==1.0.1
      - ua-parser-builtins==0.18.0.post1
      - uritemplate==4.1.1
      - urllib3==2.3.0
      - user-agents==2.2.0
      - uwsgi==2.0.28
      - werkzeug==3.1.3
      - xlsxwriter==3.2.2
      - zipp==3.21.0
prefix: /opt/conda/envs/sirepo

tests/feature_config_test.py::test_1
sirepo/simulation_db.py sirepo/feature_config.py sirepo/pkcli/admin.py
sirepo/simulation_db.py sirepo/feature_config.py sirepo/pkcli/admin.py